@cdc/core 4.25.8 → 4.25.10

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 (117) hide show
  1. package/components/AdvancedEditor/AdvancedEditor.tsx +29 -8
  2. package/components/DataTable/DataTable.tsx +56 -38
  3. package/components/DataTable/components/ChartHeader.tsx +44 -14
  4. package/components/DataTable/components/ExpandCollapse.tsx +10 -1
  5. package/components/DataTable/components/MapHeader.tsx +24 -13
  6. package/components/DataTable/data-table.css +6 -0
  7. package/components/DataTable/helpers/chartCellMatrix.tsx +11 -8
  8. package/components/DataTable/helpers/mapCellMatrix.tsx +19 -1
  9. package/components/DownloadButton.tsx +40 -14
  10. package/components/EditorPanel/components/MarkupHighlightedTextField.tsx +227 -0
  11. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +411 -0
  12. package/components/EditorPanel/components/PanelMarkup.tsx +59 -0
  13. package/components/ErrorBoundary.jsx +3 -1
  14. package/components/Filters/Filters.tsx +27 -20
  15. package/components/Filters/components/Tabs.tsx +1 -0
  16. package/components/Legend/Legend.Gradient.tsx +3 -6
  17. package/components/LegendShape.tsx +121 -3
  18. package/components/MediaControls.tsx +51 -3
  19. package/components/PaletteConversionModal.tsx +87 -0
  20. package/components/PaletteSelector/DeveloperPaletteRollback.tsx +114 -0
  21. package/components/PaletteSelector/PaletteSelector.css +51 -0
  22. package/components/PaletteSelector/PaletteSelector.tsx +112 -0
  23. package/components/PaletteSelector/index.ts +2 -0
  24. package/components/RichTooltip/RichTooltip.tsx +1 -0
  25. package/components/Table/Table.tsx +3 -1
  26. package/components/_stories/BlurStrokeTest.stories.tsx +1 -1
  27. package/components/_stories/DataTable.stories.tsx +1 -1
  28. package/components/_stories/Filters.stories.tsx +1 -1
  29. package/components/_stories/Footnotes.stories.tsx +1 -1
  30. package/components/_stories/Inputs.stories.tsx +1 -1
  31. package/components/_stories/MultiSelect.stories.tsx +3 -3
  32. package/components/_stories/NestedDropdown.stories.tsx +1 -1
  33. package/components/_stories/Table.stories.tsx +1 -1
  34. package/components/elements/_stories/Button.stories.tsx +1 -1
  35. package/components/elements/_stories/Card.stories.tsx +1 -1
  36. package/components/inputs/InputToggle.tsx +2 -0
  37. package/components/managers/DataDesigner.tsx +10 -9
  38. package/components/managers/_stories/DataDesigner.stories.tsx +1 -1
  39. package/components/ui/Tooltip.tsx +2 -1
  40. package/components/ui/_stories/Accordion.stories.tsx +1 -1
  41. package/components/ui/_stories/ColorPaletteMigration.stories.mdx +275 -0
  42. package/components/ui/_stories/Colors.stories.tsx +330 -0
  43. package/components/ui/_stories/IconGallery.stories.tsx +316 -0
  44. package/components/ui/_stories/Title.stories.tsx +1 -1
  45. package/contexts/EditorContext.ts +18 -0
  46. package/contexts/editor.actions.ts +28 -0
  47. package/contexts/editor.reducer.ts +94 -0
  48. package/data/chartColorPalettes.ts +118 -0
  49. package/data/colorPalettes.ts +9 -0
  50. package/data/mapColorPalettes.ts +45 -0
  51. package/data/sharedPalettes.ts +50 -0
  52. package/dist/cove-main.css +14 -11
  53. package/dist/cove-main.css.map +1 -1
  54. package/generateViteConfig.js +80 -0
  55. package/helpers/addValuesToFilters.ts +2 -3
  56. package/helpers/cloneConfig.ts +31 -0
  57. package/helpers/configDataHelpers.ts +128 -0
  58. package/helpers/configHelpers.ts +27 -0
  59. package/helpers/constants.ts +5 -2
  60. package/helpers/coveUpdateWorker.ts +13 -3
  61. package/helpers/filterColorPalettes.ts +152 -0
  62. package/helpers/generateColorsArray.ts +13 -0
  63. package/helpers/getColorPaletteVersion.ts +33 -0
  64. package/helpers/getPaletteAccessor.ts +18 -0
  65. package/helpers/markupProcessor.ts +205 -0
  66. package/helpers/metrics/helpers.ts +42 -19
  67. package/helpers/metrics/types.ts +48 -9
  68. package/helpers/metrics/utils.ts +34 -0
  69. package/helpers/palettes/colorDistributions.ts +56 -0
  70. package/helpers/palettes/migratePaletteName.ts +150 -0
  71. package/helpers/palettes/standardizePaletteNames.ts +77 -0
  72. package/helpers/palettes/utils.ts +267 -0
  73. package/helpers/queryStringUtils.ts +13 -0
  74. package/helpers/testing.ts +345 -0
  75. package/helpers/tests/addValuesToFilters.test.ts +1 -2
  76. package/helpers/tests/generateColorsArray.test.ts +24 -0
  77. package/helpers/tests/markupProcessor.test.ts +538 -0
  78. package/helpers/tests/testStandaloneBuild.ts +44 -0
  79. package/helpers/useMarkupVariables.ts +31 -0
  80. package/helpers/vegaConfig.ts +0 -1
  81. package/helpers/ver/4.24.10.ts +2 -1
  82. package/helpers/ver/4.24.11.ts +2 -1
  83. package/helpers/ver/4.24.3.ts +2 -1
  84. package/helpers/ver/4.24.4.ts +2 -1
  85. package/helpers/ver/4.24.5.ts +2 -1
  86. package/helpers/ver/4.24.7.ts +2 -1
  87. package/helpers/ver/4.24.9.ts +2 -1
  88. package/helpers/ver/4.25.1.ts +2 -1
  89. package/helpers/ver/4.25.10.ts +36 -0
  90. package/helpers/ver/4.25.3.ts +2 -1
  91. package/helpers/ver/4.25.4.ts +2 -1
  92. package/helpers/ver/4.25.6.ts +2 -1
  93. package/helpers/ver/4.25.7.ts +2 -1
  94. package/helpers/ver/4.25.8.ts +2 -1
  95. package/helpers/ver/4.25.9.ts +293 -0
  96. package/helpers/ver/tests/4.25.10.test.ts +204 -0
  97. package/helpers/ver/tests/4.25.8.test.ts +1 -1
  98. package/helpers/ver/tests/4.25.9.test.ts +51 -0
  99. package/hooks/useColorPalette.ts +79 -0
  100. package/package.json +12 -4
  101. package/styles/_global.scss +7 -5
  102. package/styles/base.scss +8 -5
  103. package/styles/v2/components/button.scss +4 -3
  104. package/styles/v2/components/editor.scss +2 -1
  105. package/styles/v2/layout/_data-table.scss +3 -2
  106. package/styles/v2/themes/_color-definitions.scss +18 -17
  107. package/testBuild.js +0 -0
  108. package/testing-setup.js +32 -0
  109. package/types/MarkupInclude.ts +6 -1
  110. package/types/MarkupVariable.ts +19 -0
  111. package/types/VizFilter.ts +1 -0
  112. package/vitest.config.ts +16 -0
  113. package/components/ui/_stories/Colors.stories.mdx +0 -220
  114. package/components/ui/_stories/IconGallery.stories.mdx +0 -14
  115. package/data/colorPalettes.js +0 -171
  116. package/helpers/formatConfigBeforeSave.ts +0 -135
  117. package/helpers/tests/formatConfigBeforeSave.test.ts +0 -68
@@ -0,0 +1,267 @@
1
+ import { map } from 'lodash'
2
+ import { FALLBACK_COLOR_PALETTE_V1, FALLBACK_COLOR_PALETTE_V2, USE_V2_MIGRATION } from '../constants'
3
+ import { getColorPaletteVersion } from '../getColorPaletteVersion'
4
+ import { getPaletteAccessor } from '../getPaletteAccessor'
5
+ import { chartPaletteMigrationMap } from './migratePaletteName'
6
+ import { newMapPaletteNames } from './standardizePaletteNames'
7
+
8
+ /**
9
+ * Gets the current palette name from a visualization config
10
+ * @param config - The visualization config object
11
+ * @returns The current palette name or empty string if not found
12
+ */
13
+ export const getCurrentPaletteName = (config: any): string => {
14
+ // Check new v2 format first
15
+ if (config?.general?.palette?.name) {
16
+ return config.general.palette.name
17
+ }
18
+
19
+ // Check legacy v1 formats
20
+ if (config?.palette) {
21
+ return config.palette
22
+ }
23
+
24
+ if (config?.color) {
25
+ return config.color
26
+ }
27
+
28
+ const paletteVersion = getColorPaletteVersion(config)
29
+ return paletteVersion === 1 ? FALLBACK_COLOR_PALETTE_V1 : FALLBACK_COLOR_PALETTE_V2
30
+ }
31
+
32
+ /**
33
+ * Gets the palette colors array from a visualization config
34
+ * @param config - The visualization config object
35
+ * @param colorPalettes - The color palettes object (e.g., mapColorPalettes, chartColorPalettes)
36
+ * @returns The palette colors array or empty array if not found
37
+ */
38
+ export const getPaletteColors = (config: any, colorPalettes: any): string[] => {
39
+ // First check for custom colors (v2 format)
40
+ if (config?.general?.palette?.customColors) {
41
+ return config.general.palette.customColors
42
+ }
43
+
44
+ // Get the raw palette name
45
+ let paletteName = getCurrentPaletteName(config)
46
+
47
+ // Apply v1 palette name migrations if this is a v1 config
48
+ const paletteVersion = getColorPaletteVersion(config)
49
+ if (paletteVersion === 1) {
50
+ paletteName = migratePaletteWithMap(paletteName, chartPaletteMigrationMap, true)
51
+ }
52
+
53
+ // Get the versioned palette accessor
54
+ const versionedPalettes = getPaletteAccessor(colorPalettes, config)
55
+
56
+ if (paletteName && versionedPalettes?.[paletteName]) {
57
+ return versionedPalettes[paletteName]
58
+ }
59
+
60
+ return []
61
+ }
62
+
63
+ /**
64
+ * Determines if the config is using a v1 palette configuration
65
+ * @param config - The visualization config object
66
+ * @returns True if the config is using v1 palette configuration (which would show conversion modal)
67
+ */
68
+ export const isV1Palette = (config: any): boolean => {
69
+ // If v2 migration is disabled globally, don't treat as v1 (no conversion modal)
70
+ if (!USE_V2_MIGRATION) {
71
+ return false
72
+ }
73
+
74
+ const currentVersion = getColorPaletteVersion(config)
75
+ return (
76
+ currentVersion === 1 ||
77
+ config?.general?.palette?.version === '1.0' ||
78
+ !config?.general?.palette?.version
79
+ )
80
+ }
81
+
82
+ /**
83
+ * Returns the appropriate fallback color palette based on the palette version
84
+ * @param config - The visualization config object
85
+ * @returns The fallback palette name for the detected version
86
+ */
87
+ export const getFallbackColorPalette = (config: any): string => {
88
+ const paletteVersion = getColorPaletteVersion(config)
89
+ return paletteVersion === 1 ? FALLBACK_COLOR_PALETTE_V1 : FALLBACK_COLOR_PALETTE_V2
90
+ }
91
+
92
+ /**
93
+ * Finds a palette name in a migration map using exact match first, then case-insensitive fallback
94
+ * @param paletteName - The palette name to look up
95
+ * @param migrationMap - The migration map to search in
96
+ * @returns The migrated palette name or null if not found
97
+ */
98
+ export const findPaletteInMigrationMap = (paletteName: string, migrationMap: Record<string, string>): string | null => {
99
+ // Try exact match first
100
+ if (migrationMap[paletteName]) {
101
+ return migrationMap[paletteName]
102
+ }
103
+
104
+ // Try case-insensitive match
105
+ const lowerCaseName = paletteName.toLowerCase()
106
+ const matchingKey = Object.keys(migrationMap).find(key => key.toLowerCase() === lowerCaseName)
107
+
108
+ return matchingKey ? migrationMap[matchingKey] : null
109
+ }
110
+
111
+ /**
112
+ * Handles reverse palette suffix logic for chart palettes
113
+ * @param originalPaletteName - The original palette name that may have 'reverse' suffix
114
+ * @param migratedBase - The migrated base name
115
+ * @returns The properly formatted migrated palette name with reverse suffix if needed
116
+ */
117
+ export const handleReversePalette = (originalPaletteName: string, migratedBase: string): string => {
118
+ const isReverse = originalPaletteName.endsWith('reverse')
119
+ return isReverse ? migratedBase + 'reverse' : migratedBase
120
+ }
121
+
122
+ /**
123
+ * Generic migration function that works with any migration map
124
+ * @param oldPaletteName - The palette name to migrate
125
+ * @param migrationMap - The migration map to use
126
+ * @param handleReverse - Whether to handle reverse palette suffixes (for chart palettes)
127
+ * @returns The migrated palette name or original if no migration found
128
+ */
129
+ export const migratePaletteWithMap = (
130
+ oldPaletteName: string,
131
+ migrationMap: Record<string, string>,
132
+ handleReverse: boolean = false
133
+ ): string => {
134
+ // Handle null/undefined/empty cases - maintain original behavior
135
+ if (!oldPaletteName) {
136
+ return oldPaletteName
137
+ }
138
+
139
+ if (handleReverse) {
140
+ // Chart palette logic - handle reverse suffix
141
+ const isReverse = oldPaletteName.endsWith('reverse')
142
+ const basePaletteName = isReverse ? oldPaletteName.slice(0, -7) : oldPaletteName
143
+
144
+ const migratedBase = findPaletteInMigrationMap(basePaletteName, migrationMap)
145
+ if (migratedBase) {
146
+ return handleReversePalette(oldPaletteName, migratedBase)
147
+ }
148
+ } else {
149
+ // Map palette logic - direct lookup
150
+ const migrated = findPaletteInMigrationMap(oldPaletteName, migrationMap)
151
+ if (migrated) {
152
+ return migrated
153
+ }
154
+ }
155
+
156
+ return oldPaletteName
157
+ }
158
+
159
+ /**
160
+ * Checks if a config has palette backup data available for rollback
161
+ * @param config - The visualization config object
162
+ * @returns True if backup data exists
163
+ */
164
+ export const hasPaletteBackup = (config: any): boolean => {
165
+ return !!(config?.general?.palette?.backups?.length > 0)
166
+ }
167
+
168
+ /**
169
+ * Gets the original palette name from backup data
170
+ * @param config - The visualization config object
171
+ * @returns The original palette name or null if no backup exists
172
+ */
173
+ export const getOriginalPaletteName = (config: any): string | null => {
174
+ const backups = config?.general?.palette?.backups
175
+ if (!backups || backups.length === 0) return null
176
+
177
+ // Get the most recent backup (last in array)
178
+ const latestBackup = backups[backups.length - 1]
179
+ return latestBackup?.name || null
180
+ }
181
+
182
+ /**
183
+ * Gets the original two-color palette name from backup data
184
+ * @param config - The visualization config object
185
+ * @returns The original two-color palette name or null if no backup exists
186
+ */
187
+ export const getOriginalTwoColorPaletteName = (config: any): string | null => {
188
+ const backups = config?.general?.palette?.backups
189
+ if (!backups || backups.length === 0) return null
190
+
191
+ // Find the two-color palette backup
192
+ const twoColorBackup = backups.find((backup: any) => backup.type === 'twoColor')
193
+ return twoColorBackup?.name || null
194
+ }
195
+
196
+ /**
197
+ * Checks if a config has two-color palette backup data available for rollback
198
+ * @param config - The visualization config object
199
+ * @returns True if two-color backup data exists
200
+ */
201
+ export const hasTwoColorPaletteBackup = (config: any): boolean => {
202
+ const backups = config?.general?.palette?.backups
203
+ if (!backups || backups.length === 0) return false
204
+ return backups.some((backup: any) => backup.type === 'twoColor')
205
+ }
206
+
207
+ /**
208
+ * Rolls back palette configuration to original pre-migration state
209
+ * @param config - The visualization config object to modify
210
+ * @returns True if rollback was successful, false if no backup available
211
+ */
212
+ export const rollbackPaletteToOriginal = (config: any): boolean => {
213
+ const backups = config?.general?.palette?.backups
214
+ if (!backups || backups.length === 0) {
215
+ return false
216
+ }
217
+
218
+ // Get the most recent backup
219
+ const latestBackup = backups[backups.length - 1]
220
+ if (!latestBackup?.name) {
221
+ return false
222
+ }
223
+
224
+ // Restore the original configuration
225
+ if (config.type === 'map') {
226
+ config.general.palette.name = newMapPaletteNames[latestBackup.name] || latestBackup.name
227
+ config.general.palette.version = '1.0' // Reset to v1
228
+ } else if (config.type === 'chart') {
229
+ config.general.palette.name = chartPaletteMigrationMap[latestBackup.name] || latestBackup.name
230
+ config.general.palette.version = '1.0' // Reset to v1
231
+ }
232
+
233
+ return config
234
+ }
235
+
236
+ /**
237
+ * Rolls back two-color palette configuration to original pre-migration state
238
+ * @param config - The visualization config object to modify
239
+ * @returns True if rollback was successful, false if no backup available
240
+ */
241
+ export const rollbackTwoColorPaletteToOriginal = (config: any): boolean => {
242
+ const backups = config?.general?.palette?.backups
243
+ if (!backups || backups.length === 0) {
244
+ return false
245
+ }
246
+
247
+ // Find the two-color palette backup
248
+ const twoColorBackup = backups.find((backup: any) => backup.type === 'twoColor')
249
+ if (!twoColorBackup?.name) {
250
+ return false
251
+ }
252
+
253
+ // Restore the original two-color palette configuration
254
+ if (config.twoColor) {
255
+ config.twoColor.palette = twoColorBackup.name
256
+ config.twoColor.isPaletteReversed = twoColorBackup.isReversed || false
257
+ }
258
+
259
+ // Reset to v1
260
+ if (config.general?.palette) {
261
+ config.general.palette.version = twoColorBackup.version || '1.0'
262
+ // Remove the two-color backup since we've rolled back
263
+ config.general.palette.backups = backups.filter((backup: any) => backup.type !== 'twoColor')
264
+ }
265
+
266
+ return true
267
+ }
@@ -49,3 +49,16 @@ export function removeQueryParam(key: string) {
49
49
  delete queryParams[key]
50
50
  updateQueryString(queryParams)
51
51
  }
52
+
53
+ /**
54
+ * Checks if developer mode is enabled via URL parameter
55
+ * @returns true if isCoveDeveloper URL parameter is present and truthy
56
+ */
57
+ export function isCoveDeveloperMode(): boolean {
58
+ const param = getQueryParam('isCoveDeveloper')
59
+ if (!param) return false
60
+
61
+ // Handle various truthy values
62
+ const lowerParam = param.toLowerCase()
63
+ return lowerParam === 'true' || lowerParam === '1' || lowerParam === 'yes'
64
+ }
@@ -0,0 +1,345 @@
1
+ import { expect, userEvent } from 'storybook/test'
2
+ import { waitFor } from '@testing-library/react'
3
+
4
+ /**
5
+ * Shared Testing Helpers for Editor Story Tests
6
+ *
7
+ * This file contains common testing utilities used across all visualization
8
+ * editor story tests (Chart, Map, Data Bite, Waffle Chart, etc.).
9
+ *
10
+ * Key Features:
11
+ * - Adaptive animation delays for test environments vs UI
12
+ * - Robust polling utilities for async state changes
13
+ * - Generic before/after assertion patterns
14
+ * - Element presence/absence waiting
15
+ * - Text content change detection
16
+ * - Editor loading and accordion interaction helpers
17
+ *
18
+ */
19
+
20
+ // ============================================================================
21
+ // ENVIRONMENT DETECTION & TIMING
22
+ // ============================================================================
23
+
24
+ /**
25
+ * Use 500ms delay for visual perception in Storybook UI, but skip in automated tests
26
+ * This ensures smooth visual feedback for manual testing while keeping automated tests fast
27
+ */
28
+ export const MIN_ANIMATION_DELAY_MS = (() => {
29
+ // Check if we're in automated test environment (Vitest/Jest)
30
+ if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
31
+ return 0
32
+ }
33
+
34
+ // Check if we're running via test runner (Vitest has __vitest__ global)
35
+ if (typeof globalThis !== 'undefined' && '__vitest__' in globalThis) {
36
+ return 0
37
+ }
38
+
39
+ return 500
40
+ })()
41
+
42
+ const WAIT_FOR_TIMEOUT_MS = 5000
43
+
44
+ // ============================================================================
45
+ // CORE POLLING UTILITIES
46
+ // ============================================================================
47
+
48
+ // Wrapper for waitFor that includes explicit animation delay
49
+ const waitForWithDelay = async (predicate: () => void, options?: { timeout?: number }) => {
50
+ if (MIN_ANIMATION_DELAY_MS > 0) {
51
+ await new Promise(resolve => setTimeout(resolve, MIN_ANIMATION_DELAY_MS))
52
+ }
53
+
54
+ await waitFor(predicate, {
55
+ timeout: options?.timeout || WAIT_FOR_TIMEOUT_MS
56
+ })
57
+ }
58
+
59
+ /**
60
+ * Perform an action and assert the result using polling
61
+ * This is the core pattern for testing state changes in editor components
62
+ *
63
+ * @param label Descriptive label for the test action
64
+ * @param read Function that reads the current state
65
+ * @param act Function that performs the action
66
+ * @param predicate Function that validates the state change
67
+ * @param extraAssert Optional additional assertion on the final value
68
+ */
69
+ export const performAndAssert = async <T extends unknown>(
70
+ label: string,
71
+ read: () => T,
72
+ act: () => Promise<void> | void,
73
+ predicate: (before: T, after: T) => boolean,
74
+ extraAssert?: (after: T) => void
75
+ ): Promise<void> => {
76
+ // Capture the call site stack trace
77
+ const callSite = new Error().stack
78
+
79
+ const before = read()
80
+ await act()
81
+
82
+ await waitForWithDelay(() => {
83
+ const after = read()
84
+ const result = predicate(before, after)
85
+ if (!result) {
86
+ // Create a more informative error with the original call site
87
+ const error = new Error(
88
+ `${label} failed: Expected predicate to return true. Before: ${JSON.stringify(before)}, After: ${JSON.stringify(
89
+ after
90
+ )}`
91
+ )
92
+
93
+ // Try to preserve the original call site in the stack trace
94
+ if (callSite) {
95
+ const originalStack = callSite.split('\n')
96
+ // Replace the generic helper stack with the original call site
97
+ if (originalStack.length > 2) {
98
+ error.stack = [
99
+ error.message,
100
+ ...originalStack.slice(2) // Skip the first two lines (error creation in helper)
101
+ ].join('\n')
102
+ }
103
+ }
104
+
105
+ throw error
106
+ }
107
+ if (extraAssert) extraAssert(after)
108
+ })
109
+ }
110
+
111
+ // ============================================================================
112
+ // ELEMENT WAITING UTILITIES
113
+ // ============================================================================
114
+
115
+ /**
116
+ * Wait for an element to appear in the DOM
117
+ *
118
+ * @param selector CSS selector for the element
119
+ * @param canvasElement Container element to search within
120
+ * @returns Promise that resolves to the found element
121
+ */
122
+ export const waitForPresence = async (selector: string, canvasElement: HTMLElement) => {
123
+ await waitForWithDelay(() => {
124
+ const element = canvasElement.querySelector(selector)
125
+ expect(element).toBeTruthy()
126
+ })
127
+ return canvasElement.querySelector(selector)
128
+ }
129
+
130
+ /**
131
+ * Wait for an element to disappear from the DOM
132
+ *
133
+ * @param selector CSS selector for the element
134
+ * @param canvasElement Container element to search within
135
+ */
136
+ export const waitForAbsence = async (selector: string, canvasElement: HTMLElement) => {
137
+ await waitForWithDelay(() => {
138
+ const element = canvasElement.querySelector(selector)
139
+ expect(element).toBeFalsy()
140
+ })
141
+ }
142
+
143
+ /**
144
+ * Wait for a select element's options to populate
145
+ *
146
+ * @param selectElement The select element to monitor
147
+ * @param minCount The minimum number of expected elements
148
+ */
149
+ export const waitForOptionsToPopulate = async (selectElement: HTMLSelectElement, minCount: number = 2) => {
150
+ await waitForWithDelay(() => {
151
+ expect(selectElement.options.length).toBeGreaterThanOrEqual(minCount)
152
+ })
153
+ }
154
+
155
+ /**
156
+ * Wait for an element's text content to change to a specific value
157
+ *
158
+ * @param el The element to monitor
159
+ * @param expected The expected text content
160
+ */
161
+ export const waitForTextContent = async (el: HTMLElement | null, expected: string) => {
162
+ expect(el).toBeTruthy()
163
+ await waitForWithDelay(() => {
164
+ expect(el!.textContent?.trim()).toBe(expected.trim())
165
+ })
166
+ }
167
+
168
+ // ============================================================================
169
+ // EDITOR-SPECIFIC UTILITIES
170
+ // ============================================================================
171
+
172
+ /**
173
+ * Wait for the editor to load and become interactive
174
+ * This ensures all accordion sections are present and the component is ready for testing
175
+ *
176
+ * @param canvas Storybook canvas object from within(canvasElement)
177
+ */
178
+ export const waitForEditor = async (canvas: any) => {
179
+ await waitForWithDelay(() => {
180
+ const editorElement = canvas.queryAllByText(/general|data|visual/i)
181
+ expect(editorElement[0]).toBeVisible()
182
+ })
183
+ }
184
+
185
+ /**
186
+ * Open an accordion section in the editor
187
+ *
188
+ * @param canvas Storybook canvas object from within(canvasElement)
189
+ * @param sectionName Name of the accordion section (case-insensitive)
190
+ */
191
+ export const openAccordion = async (canvas: any, sectionName: string) => {
192
+ const accordion = canvas.getByRole('button', { name: new RegExp(sectionName, 'i') })
193
+ await userEvent.click(accordion)
194
+ await waitForWithDelay(() => {
195
+ const accordionContent = accordion.closest('.accordion-item, .accordion-section, [class*="accordion"]')
196
+ expect(accordionContent).toBeTruthy()
197
+ })
198
+ }
199
+
200
+ // ============================================================================
201
+ // VISUAL STATE HELPERS
202
+ // ============================================================================
203
+
204
+ /**
205
+ * Get visual state for boolean/checkbox testing
206
+ * This is a flexible helper that can be customized for different visual properties
207
+ *
208
+ * @param element The element to analyze
209
+ * @param options Configuration for what visual properties to check
210
+ * @returns Object containing the visual state
211
+ */
212
+ export const getVisualState = (
213
+ element: HTMLElement | null,
214
+ options: {
215
+ checkClasses?: string[] // CSS classes to check for presence
216
+ checkStyles?: string[] // CSS properties to read from computed styles
217
+ checkAttributes?: string[] // HTML attributes to read
218
+ customChecks?: ((el: HTMLElement) => Record<string, any>)[] // Custom visual checks
219
+ } = {}
220
+ ) => {
221
+ if (!element) return null
222
+
223
+ const state: Record<string, any> = {}
224
+
225
+ // Check for specific CSS classes
226
+ if (options.checkClasses) {
227
+ for (const className of options.checkClasses) {
228
+ state[`has_${className.replace(/[^a-zA-Z0-9]/g, '_')}`] = element.classList.contains(className)
229
+ }
230
+ }
231
+
232
+ // Check computed styles
233
+ if (options.checkStyles) {
234
+ const computedStyle = getComputedStyle(element)
235
+ for (const property of options.checkStyles) {
236
+ state[`style_${property.replace(/[^a-zA-Z0-9]/g, '_')}`] = computedStyle.getPropertyValue(property)
237
+ }
238
+ }
239
+
240
+ // Check attributes
241
+ if (options.checkAttributes) {
242
+ for (const attr of options.checkAttributes) {
243
+ state[`attr_${attr.replace(/[^a-zA-Z0-9]/g, '_')}`] = element.getAttribute(attr)
244
+ }
245
+ }
246
+
247
+ // Run custom checks
248
+ if (options.customChecks) {
249
+ for (const customCheck of options.customChecks) {
250
+ Object.assign(state, customCheck(element))
251
+ }
252
+ }
253
+
254
+ // Always include basic visibility state
255
+ state.isVisible = element.offsetParent !== null
256
+ state.classList = Array.from(element.classList).sort().join(' ')
257
+
258
+ return state
259
+ }
260
+
261
+ /**
262
+ * Create a comprehensive boolean testing function for checkbox controls
263
+ * Tests both enable/disable directions and verifies visual changes
264
+ *
265
+ * @param checkbox The checkbox element to test
266
+ * @param getVisualState Function that returns the current visual state
267
+ * @param testName Descriptive name for the test
268
+ */
269
+ export const testBooleanControl = async (checkbox: HTMLInputElement, getVisualState: () => any, testName: string) => {
270
+ // Get initial state
271
+ const initialCheckboxState = checkbox.checked
272
+ const initialVisualState = getVisualState()
273
+
274
+ // First toggle: change to opposite state
275
+ await userEvent.click(checkbox)
276
+ await new Promise(resolve => setTimeout(resolve, 100))
277
+ const firstToggleCheckboxState = checkbox.checked
278
+ const firstToggleVisualState = getVisualState()
279
+
280
+ // Verify checkbox changed
281
+ expect(firstToggleCheckboxState).not.toBe(initialCheckboxState)
282
+
283
+ // Verify visualization changed (with flexible handling)
284
+ if (JSON.stringify(firstToggleVisualState) === JSON.stringify(initialVisualState)) {
285
+ // Note: This warns but doesn't fail, allowing for controls that may not have
286
+ // visible effects under current data/settings
287
+ console.warn(`⚠️ ${testName}: Checkbox toggled but no visual change detected`)
288
+ } else {
289
+ expect(firstToggleVisualState).not.toEqual(initialVisualState)
290
+ }
291
+
292
+ // Second toggle: return to original state
293
+ await userEvent.click(checkbox)
294
+ await new Promise(resolve => setTimeout(resolve, 100))
295
+ const secondToggleCheckboxState = checkbox.checked
296
+ const secondToggleVisualState = getVisualState()
297
+
298
+ // Verify both checkbox AND visualization returned to original
299
+ expect(secondToggleCheckboxState).toBe(initialCheckboxState)
300
+ expect(secondToggleVisualState).toEqual(initialVisualState)
301
+ }
302
+
303
+ // ============================================================================
304
+ // DATA EXTRACTION HELPERS
305
+ // ============================================================================
306
+
307
+ /**
308
+ * Get the primary data value displayed in a visualization
309
+ * Tries multiple common selectors for data values
310
+ *
311
+ * @param canvasElement The container element to search within
312
+ * @returns The data value as a string, or empty string if not found
313
+ */
314
+ export const getDisplayValue = (canvasElement: HTMLElement): string => {
315
+ const selectors = ['svg text', '.bite-text', '.data-value', '[data-testid="data-value"]']
316
+
317
+ for (const selector of selectors) {
318
+ const elements = canvasElement.querySelectorAll(selector)
319
+ for (let i = 0; i < elements.length; i++) {
320
+ const element = elements[i]
321
+ const text = element.textContent?.trim()
322
+ if (text && /[\d,]+/.test(text)) {
323
+ return text
324
+ }
325
+ }
326
+ }
327
+
328
+ return ''
329
+ }
330
+
331
+ /**
332
+ * Get the title text from a visualization
333
+ * Tries multiple common selectors for titles
334
+ *
335
+ * @param canvas Storybook canvas object from within(canvasElement)
336
+ * @returns The title text as a string, or empty string if not found
337
+ */
338
+ export const getTitleText = (canvas: any): string => {
339
+ try {
340
+ const titleElement = canvas.getByRole('heading') || canvas.querySelector('h1, h2, h3, .title')
341
+ return titleElement?.textContent?.trim() || ''
342
+ } catch {
343
+ return ''
344
+ }
345
+ }
@@ -2,7 +2,6 @@ import _ from 'lodash'
2
2
  import { VizFilter } from '../../types/VizFilter'
3
3
  import { addValuesToFilters } from '../addValuesToFilters'
4
4
  import { describe, it, expect, vi } from 'vitest'
5
- import { FILTER_STYLE } from '@cdc/dashboard/src/types/FilterStyles'
6
5
 
7
6
  describe('addValuesToFilters', () => {
8
7
  const parentFilter = { columnName: 'parentColumn', id: 11, active: 'apple', values: [] } as VizFilter
@@ -52,7 +51,7 @@ describe('addValuesToFilters', () => {
52
51
  it('works for nested dropdowns', () => {
53
52
  const nestedParentFilter = {
54
53
  ...parentFilter,
55
- filterStyle: FILTER_STYLE.nestedDropdown,
54
+ filterStyle: 'nested-dropdown',
56
55
  subGrouping: { columnName: 'childColumn' }
57
56
  }
58
57
  const newFilters = addValuesToFilters([nestedParentFilter], data)
@@ -0,0 +1,24 @@
1
+ import { generateColorsArray } from '../generateColorsArray'
2
+ import { describe, it, expect } from 'vitest'
3
+
4
+ describe('generateColorsArray', () => {
5
+ it('should return an array of colors', () => {
6
+ const colors = generateColorsArray('#fde0dd', false)
7
+ expect(colors).toEqual(expect.arrayContaining(['#fde0dd', '#ffd0c9', '#edd1ce']))
8
+ })
9
+
10
+ it('should return a brighter hover color when special flag is true', () => {
11
+ const colors = generateColorsArray('#fde0dd', true)
12
+ expect(colors[1]).toEqual('#fffaf7')
13
+ })
14
+
15
+ it('should return a darker color for the third element in the array', () => {
16
+ const colors = generateColorsArray('#fde0dd', false)
17
+ expect(colors[2]).toBe('#edd1ce')
18
+ })
19
+
20
+ it('should use black as default color', () => {
21
+ const colors = generateColorsArray()
22
+ expect(colors[0]).toBe('#000000')
23
+ })
24
+ })