@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.
- package/components/AdvancedEditor/AdvancedEditor.tsx +29 -8
- package/components/DataTable/DataTable.tsx +56 -38
- package/components/DataTable/components/ChartHeader.tsx +44 -14
- package/components/DataTable/components/ExpandCollapse.tsx +10 -1
- package/components/DataTable/components/MapHeader.tsx +24 -13
- package/components/DataTable/data-table.css +6 -0
- package/components/DataTable/helpers/chartCellMatrix.tsx +11 -8
- package/components/DataTable/helpers/mapCellMatrix.tsx +19 -1
- package/components/DownloadButton.tsx +40 -14
- package/components/EditorPanel/components/MarkupHighlightedTextField.tsx +227 -0
- package/components/EditorPanel/components/MarkupVariablesEditor.tsx +411 -0
- package/components/EditorPanel/components/PanelMarkup.tsx +59 -0
- package/components/ErrorBoundary.jsx +3 -1
- package/components/Filters/Filters.tsx +27 -20
- package/components/Filters/components/Tabs.tsx +1 -0
- package/components/Legend/Legend.Gradient.tsx +3 -6
- package/components/LegendShape.tsx +121 -3
- package/components/MediaControls.tsx +51 -3
- package/components/PaletteConversionModal.tsx +87 -0
- package/components/PaletteSelector/DeveloperPaletteRollback.tsx +114 -0
- package/components/PaletteSelector/PaletteSelector.css +51 -0
- package/components/PaletteSelector/PaletteSelector.tsx +112 -0
- package/components/PaletteSelector/index.ts +2 -0
- package/components/RichTooltip/RichTooltip.tsx +1 -0
- package/components/Table/Table.tsx +3 -1
- package/components/_stories/BlurStrokeTest.stories.tsx +1 -1
- package/components/_stories/DataTable.stories.tsx +1 -1
- package/components/_stories/Filters.stories.tsx +1 -1
- package/components/_stories/Footnotes.stories.tsx +1 -1
- package/components/_stories/Inputs.stories.tsx +1 -1
- package/components/_stories/MultiSelect.stories.tsx +3 -3
- package/components/_stories/NestedDropdown.stories.tsx +1 -1
- package/components/_stories/Table.stories.tsx +1 -1
- package/components/elements/_stories/Button.stories.tsx +1 -1
- package/components/elements/_stories/Card.stories.tsx +1 -1
- package/components/inputs/InputToggle.tsx +2 -0
- package/components/managers/DataDesigner.tsx +10 -9
- package/components/managers/_stories/DataDesigner.stories.tsx +1 -1
- package/components/ui/Tooltip.tsx +2 -1
- package/components/ui/_stories/Accordion.stories.tsx +1 -1
- package/components/ui/_stories/ColorPaletteMigration.stories.mdx +275 -0
- package/components/ui/_stories/Colors.stories.tsx +330 -0
- package/components/ui/_stories/IconGallery.stories.tsx +316 -0
- package/components/ui/_stories/Title.stories.tsx +1 -1
- package/contexts/EditorContext.ts +18 -0
- package/contexts/editor.actions.ts +28 -0
- package/contexts/editor.reducer.ts +94 -0
- package/data/chartColorPalettes.ts +118 -0
- package/data/colorPalettes.ts +9 -0
- package/data/mapColorPalettes.ts +45 -0
- package/data/sharedPalettes.ts +50 -0
- package/dist/cove-main.css +14 -11
- package/dist/cove-main.css.map +1 -1
- package/generateViteConfig.js +80 -0
- package/helpers/addValuesToFilters.ts +2 -3
- package/helpers/cloneConfig.ts +31 -0
- package/helpers/configDataHelpers.ts +128 -0
- package/helpers/configHelpers.ts +27 -0
- package/helpers/constants.ts +5 -2
- package/helpers/coveUpdateWorker.ts +13 -3
- package/helpers/filterColorPalettes.ts +152 -0
- package/helpers/generateColorsArray.ts +13 -0
- package/helpers/getColorPaletteVersion.ts +33 -0
- package/helpers/getPaletteAccessor.ts +18 -0
- package/helpers/markupProcessor.ts +205 -0
- package/helpers/metrics/helpers.ts +42 -19
- package/helpers/metrics/types.ts +48 -9
- package/helpers/metrics/utils.ts +34 -0
- package/helpers/palettes/colorDistributions.ts +56 -0
- package/helpers/palettes/migratePaletteName.ts +150 -0
- package/helpers/palettes/standardizePaletteNames.ts +77 -0
- package/helpers/palettes/utils.ts +267 -0
- package/helpers/queryStringUtils.ts +13 -0
- package/helpers/testing.ts +345 -0
- package/helpers/tests/addValuesToFilters.test.ts +1 -2
- package/helpers/tests/generateColorsArray.test.ts +24 -0
- package/helpers/tests/markupProcessor.test.ts +538 -0
- package/helpers/tests/testStandaloneBuild.ts +44 -0
- package/helpers/useMarkupVariables.ts +31 -0
- package/helpers/vegaConfig.ts +0 -1
- package/helpers/ver/4.24.10.ts +2 -1
- package/helpers/ver/4.24.11.ts +2 -1
- package/helpers/ver/4.24.3.ts +2 -1
- package/helpers/ver/4.24.4.ts +2 -1
- package/helpers/ver/4.24.5.ts +2 -1
- package/helpers/ver/4.24.7.ts +2 -1
- package/helpers/ver/4.24.9.ts +2 -1
- package/helpers/ver/4.25.1.ts +2 -1
- package/helpers/ver/4.25.10.ts +36 -0
- package/helpers/ver/4.25.3.ts +2 -1
- package/helpers/ver/4.25.4.ts +2 -1
- package/helpers/ver/4.25.6.ts +2 -1
- package/helpers/ver/4.25.7.ts +2 -1
- package/helpers/ver/4.25.8.ts +2 -1
- package/helpers/ver/4.25.9.ts +293 -0
- package/helpers/ver/tests/4.25.10.test.ts +204 -0
- package/helpers/ver/tests/4.25.8.test.ts +1 -1
- package/helpers/ver/tests/4.25.9.test.ts +51 -0
- package/hooks/useColorPalette.ts +79 -0
- package/package.json +12 -4
- package/styles/_global.scss +7 -5
- package/styles/base.scss +8 -5
- package/styles/v2/components/button.scss +4 -3
- package/styles/v2/components/editor.scss +2 -1
- package/styles/v2/layout/_data-table.scss +3 -2
- package/styles/v2/themes/_color-definitions.scss +18 -17
- package/testBuild.js +0 -0
- package/testing-setup.js +32 -0
- package/types/MarkupInclude.ts +6 -1
- package/types/MarkupVariable.ts +19 -0
- package/types/VizFilter.ts +1 -0
- package/vitest.config.ts +16 -0
- package/components/ui/_stories/Colors.stories.mdx +0 -220
- package/components/ui/_stories/IconGallery.stories.mdx +0 -14
- package/data/colorPalettes.js +0 -171
- package/helpers/formatConfigBeforeSave.ts +0 -135
- 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:
|
|
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
|
+
})
|