@cdc/core 4.25.11 → 4.26.2
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/.claude/agents/qa-test-developer.md +126 -0
- package/CLAUDE.local.md +67 -0
- package/_stories/Gallery.Charts.stories.tsx +300 -0
- package/_stories/Gallery.DataBite.stories.tsx +79 -0
- package/_stories/Gallery.Maps.stories.tsx +239 -0
- package/_stories/Gallery.WaffleChart.stories.tsx +187 -0
- package/_stories/PageART.stories.tsx +193 -0
- package/_stories/PageBRFSS.stories.tsx +294 -0
- package/_stories/PageCancerRegistries.stories.tsx +199 -0
- package/_stories/PageEasternEquineEncephalitis.stories.tsx +216 -0
- package/_stories/PageExcessiveAlcoholUse.stories.tsx +201 -0
- package/_stories/PageMaternalMortality.stories.tsx +193 -0
- package/_stories/PageOralHealth.stories.tsx +201 -0
- package/_stories/PageRespiratory.stories.tsx +332 -0
- package/_stories/PageSmokingTobacco.stories.tsx +200 -0
- package/_stories/PageStateDiabetesProfiles.stories.tsx +201 -0
- package/_stories/PageWastewater.stories.tsx +477 -0
- package/_stories/VegaImport.stories.tsx +401 -0
- package/_stories/vega-fixtures/bars-with-line.json +444 -0
- package/_stories/vega-fixtures/bars.json +58 -0
- package/_stories/vega-fixtures/combo-bar-rolling-mean.json +88 -0
- package/_stories/vega-fixtures/combo.json +68 -0
- package/_stories/vega-fixtures/grouped-horizontal-bars.json +83 -0
- package/_stories/vega-fixtures/grouped-horizontal-bars2.json +231 -0
- package/_stories/vega-fixtures/horizontal-bar.json +427 -0
- package/_stories/vega-fixtures/horizontal-bars-with-bad-colors.json +197 -0
- package/_stories/vega-fixtures/horizontal-bars2.json +58 -0
- package/_stories/vega-fixtures/lines.json +227 -0
- package/_stories/vega-fixtures/measles-bars.json +348 -0
- package/_stories/vega-fixtures/measles-map.json +11101 -0
- package/_stories/vega-fixtures/measles-stacked-bars.json +2147 -0
- package/_stories/vega-fixtures/multi-dataset.json +255 -0
- package/_stories/vega-fixtures/no-data.json +14 -0
- package/_stories/vega-fixtures/pie-chart.json +94 -0
- package/_stories/vega-fixtures/repeat-spec.json +47 -0
- package/_stories/vega-fixtures/stacked-area.json +222 -0
- package/_stories/vega-fixtures/stacked-bar-with-rect.json +3412 -0
- package/_stories/vega-fixtures/stacked-bars-with-line.json +364 -0
- package/_stories/vega-fixtures/stacked-bars.json +212 -0
- package/_stories/vega-fixtures/stacked-horizontal-bars.json +140 -0
- package/_stories/vega-fixtures/warning-combo.json +59 -0
- package/_stories/vega-fixtures/warning-scatter-and-line.json +1182 -0
- package/assets/icon-chart-area.svg +1 -0
- package/assets/icon-chart-radar.svg +23 -0
- package/assets/icon-magnifying-glass.svg +5 -0
- package/assets/icon-warming-stripes.svg +13 -0
- package/assets/logo2.svg +31 -0
- package/components/AdvancedEditor/AdvancedEditor.tsx +4 -0
- package/components/AdvancedEditor/EmbedEditor.tsx +513 -0
- package/components/ComboBox/ComboBox.tsx +345 -0
- package/components/ComboBox/combobox.styles.css +185 -0
- package/components/ComboBox/index.ts +1 -0
- package/components/CustomColorsEditor/CustomColorsEditor.tsx +3 -10
- package/components/DataTable/DataTable.tsx +132 -58
- package/components/DataTable/data-table.css +216 -215
- package/components/DataTable/helpers/getSeriesName.ts +6 -0
- package/components/DataTable/helpers/mapCellMatrix.tsx +14 -6
- package/components/EditorPanel/ColumnsEditor.tsx +37 -19
- package/components/EditorPanel/DataTableEditor.tsx +51 -25
- package/components/EditorPanel/EditorPanel.styles.css +16 -0
- package/components/EditorPanel/EditorPanel.tsx +144 -0
- package/components/EditorPanel/EditorPanelDispatch.tsx +75 -0
- package/components/EditorPanel/FieldSetWrapper.tsx +66 -23
- package/components/EditorPanel/Inputs.tsx +33 -7
- package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +14 -6
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +240 -175
- package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +33 -29
- package/components/EditorPanel/sections/VisualSection.tsx +169 -0
- package/components/Filters/Filters.tsx +31 -5
- package/components/Filters/helpers/getNestedOptions.ts +2 -1
- package/components/Filters/helpers/handleSorting.ts +1 -1
- package/components/Layout/components/Sidebar/components/sidebar.styles.scss +84 -2
- package/components/Layout/components/Visualization/index.tsx +27 -1
- package/components/Layout/components/Visualization/visualizations.scss +7 -0
- package/components/Legend/Legend.Gradient.tsx +1 -1
- package/components/MediaControls.tsx +53 -28
- package/components/_stories/CustomColorsEditor.stories.tsx +37 -0
- package/components/_stories/DataTable.stories.tsx +1 -0
- package/components/ui/Icon.tsx +3 -1
- package/components/ui/Title/index.tsx +30 -2
- package/components/ui/Title/title.styles.css +42 -0
- package/data/colorPalettes.ts +18 -5
- package/data/mapColorPalettes.ts +10 -0
- package/devTemplate/dev.js +235 -0
- package/devTemplate/index.html +30 -0
- package/devTemplate/preview.html +1503 -0
- package/devTemplate/sidebar.css +151 -0
- package/dist/cove-main.css +2803 -4448
- package/dist/cove-main.css.map +1 -1
- package/generateViteConfig.js +118 -2
- package/helpers/DataTransform.ts +1 -5
- package/helpers/addValuesToFilters.ts +6 -1
- package/helpers/cove/date.ts +33 -1
- package/helpers/cove/string.ts +29 -0
- package/helpers/coveUpdateWorker.ts +21 -12
- package/helpers/embed/embedCodeGenerator.ts +80 -0
- package/helpers/embed/embedHelper.js +158 -0
- package/helpers/embed/filterUtils.ts +121 -0
- package/helpers/embed/index.ts +21 -0
- package/helpers/embed/urlValidation.ts +119 -0
- package/helpers/filterVizData.ts +6 -1
- package/helpers/getFileExtension.ts +0 -6
- package/helpers/getUniqueValues.ts +19 -0
- package/helpers/hashObj.ts +25 -0
- package/helpers/isRightAlignedTableValue.js +5 -0
- package/helpers/metrics/helpers.ts +1 -0
- package/helpers/metrics/types.ts +3 -0
- package/helpers/palettes/colorDistributions.ts +1 -1
- package/helpers/palettes/utils.ts +12 -12
- package/helpers/parseCsvWithQuotes.ts +15 -14
- package/helpers/pivotData.ts +2 -2
- package/helpers/prepareScreenshot.ts +288 -0
- package/helpers/queryStringUtils.ts +29 -0
- package/helpers/testing.ts +44 -0
- package/helpers/tests/DataTransform.test.ts +125 -0
- package/helpers/tests/date.test.ts +64 -0
- package/helpers/tests/prepareScreenshot.test.ts +414 -0
- package/helpers/tests/queryStringUtils.test.ts +381 -0
- package/helpers/tests/testStandaloneBuild.ts +23 -5
- package/helpers/useDataVizClasses.ts +0 -1
- package/helpers/vegaConfig.ts +1 -1
- package/helpers/vegaConfigImport.ts +160 -0
- package/helpers/ver/4.26.1.ts +80 -0
- package/helpers/ver/4.26.2.ts +84 -0
- package/helpers/ver/tests/4.26.1.test.ts +105 -0
- package/helpers/ver/tests/4.26.2.test.ts +298 -0
- package/helpers/viewports.ts +2 -0
- package/hooks/useDataColumns.ts +63 -0
- package/hooks/useFilterManagement.ts +94 -0
- package/hooks/useLegendSeparators.ts +26 -0
- package/hooks/useListManagement.ts +192 -0
- package/package.json +29 -33
- package/styles/_button-section.scss +0 -3
- package/styles/v2/components/editor.scss +9 -9
- package/styles/v2/utils/_grid.scss +8 -3
- package/types/Annotation.ts +10 -11
- package/types/Axis.ts +1 -0
- package/types/ForecastingSeriesKey.ts +1 -0
- package/types/General.ts +2 -0
- package/types/MarkupInclude.ts +1 -0
- package/types/Palette.ts +21 -0
- package/types/Series.ts +3 -0
- package/types/Table.ts +1 -0
- package/types/Visualization.ts +7 -0
- package/types/VizFilter.ts +1 -0
- package/LICENSE +0 -201
- package/_stories/StoryRenderingTests.stories.tsx +0 -164
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { within, expect } from 'storybook/test'
|
|
3
|
+
import Dashboard from '@cdc/dashboard'
|
|
4
|
+
import { useEffect, useState } from 'react'
|
|
5
|
+
|
|
6
|
+
// Fallback step function for test descriptions
|
|
7
|
+
const step = async (description: string, fn: () => Promise<void> | void) => {
|
|
8
|
+
console.log(`▶ ${description}`)
|
|
9
|
+
await fn()
|
|
10
|
+
console.log(`✓ ${description}`)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const meta: Meta = {
|
|
14
|
+
title: 'Regression Tests/Pages/BRFSS',
|
|
15
|
+
parameters: {
|
|
16
|
+
layout: 'fullscreen',
|
|
17
|
+
docs: {
|
|
18
|
+
description: {
|
|
19
|
+
component:
|
|
20
|
+
'Stories for visualizations from the CDC Behavioral Risk Factor Surveillance System (BRFSS) Prevalence Data & Data Analysis Tools page (https://www.cdc.gov/brfss/brfssprevalence/index.html)'
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
tags: ['autodocs']
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default meta
|
|
28
|
+
|
|
29
|
+
// Config URLs from the BRFSS prevalence page
|
|
30
|
+
const CONFIG_URLS = {
|
|
31
|
+
exploreByLocation: 'https://www.cdc.gov/brfss/brfssprevalence/modules/explore-by-location.json',
|
|
32
|
+
exploreByTopic: 'https://www.cdc.gov/brfss/brfssprevalence/modules/explore-by-topic.json'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Helper to fetch config and update data URLs to use absolute cdc.gov paths
|
|
36
|
+
const useConfigWithAbsoluteDataUrl = (configUrl: string) => {
|
|
37
|
+
const [config, setConfig] = useState(null)
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
fetch(configUrl)
|
|
41
|
+
.then(res => res.json())
|
|
42
|
+
.then(data => {
|
|
43
|
+
// Convert relative data URLs to absolute cdc.gov URLs
|
|
44
|
+
if (data.dataUrl) {
|
|
45
|
+
// Handle different relative path formats (../../path or /path)
|
|
46
|
+
const dataUrl = data.dataUrl.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
|
|
47
|
+
data.dataUrl = `https://www.cdc.gov/${dataUrl}`
|
|
48
|
+
}
|
|
49
|
+
if (data.dataFileName) {
|
|
50
|
+
const dataFileName = data.dataFileName.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
|
|
51
|
+
data.dataFileName = `https://www.cdc.gov/${dataFileName}`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// For dashboard configs with multiDashboards, convert dataKey references in visualizations
|
|
55
|
+
if (data.multiDashboards) {
|
|
56
|
+
data.multiDashboards.forEach((dashboard: any) => {
|
|
57
|
+
if (dashboard.visualizations) {
|
|
58
|
+
Object.values(dashboard.visualizations).forEach((viz: any) => {
|
|
59
|
+
// Only convert dataKey if it's a URL path (starts with / or ../)
|
|
60
|
+
if (viz.dataKey && (viz.dataKey.startsWith('/') || viz.dataKey.startsWith('../'))) {
|
|
61
|
+
const dataKey = viz.dataKey.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
|
|
62
|
+
viz.dataKey = `https://www.cdc.gov/${dataKey}`
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// For dashboard configs, convert dataKey references in visualizations
|
|
70
|
+
if (data.visualizations) {
|
|
71
|
+
Object.values(data.visualizations).forEach((viz: any) => {
|
|
72
|
+
// Only convert dataKey if it's a URL path (starts with / or ../)
|
|
73
|
+
if (viz.dataKey && (viz.dataKey.startsWith('/') || viz.dataKey.startsWith('../'))) {
|
|
74
|
+
const dataKey = viz.dataKey.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
|
|
75
|
+
viz.dataKey = `https://www.cdc.gov/${dataKey}`
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// For dashboard configs, convert datasets only if they reference external files
|
|
81
|
+
if (data.datasets) {
|
|
82
|
+
const newDatasets = {}
|
|
83
|
+
Object.entries(data.datasets).forEach(([key, dataset]: [string, any]) => {
|
|
84
|
+
// Check if dataset has embedded data
|
|
85
|
+
const hasEmbeddedData = (dataset as any).data && Array.isArray((dataset as any).data)
|
|
86
|
+
|
|
87
|
+
// If data is embedded, keep the original key
|
|
88
|
+
if (hasEmbeddedData) {
|
|
89
|
+
newDatasets[key] = dataset
|
|
90
|
+
} else {
|
|
91
|
+
// Otherwise, convert paths to absolute URLs (but keep absolute URLs as-is)
|
|
92
|
+
const newKey = key.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
|
|
93
|
+
const absoluteKey = key.startsWith('http') ? key : `https://www.cdc.gov/${newKey}`
|
|
94
|
+
|
|
95
|
+
newDatasets[absoluteKey] = {
|
|
96
|
+
...dataset,
|
|
97
|
+
dataFileName:
|
|
98
|
+
(dataset as any).dataFileName && !(dataset as any).dataFileName.startsWith('http')
|
|
99
|
+
? `https://www.cdc.gov/${(dataset as any).dataFileName
|
|
100
|
+
.replace(/^(\.\.\/)+/, '')
|
|
101
|
+
.replace(/^\//, '')}`
|
|
102
|
+
: (dataset as any).dataFileName,
|
|
103
|
+
dataUrl:
|
|
104
|
+
(dataset as any).dataUrl && !(dataset as any).dataUrl.startsWith('http')
|
|
105
|
+
? `https://www.cdc.gov/${(dataset as any).dataUrl.replace(/^(\.\.\/)+/, '').replace(/^\//, '')}`
|
|
106
|
+
: (dataset as any).dataUrl
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
data.datasets = newDatasets
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Set activeDashboard to 0 if it's null and multiDashboards exist
|
|
114
|
+
if (data.multiDashboards && data.multiDashboards.length > 0 && data.activeDashboard === null) {
|
|
115
|
+
data.activeDashboard = 0
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Log config info for debugging
|
|
119
|
+
console.log('✓ Config loaded:', {
|
|
120
|
+
type: data.type,
|
|
121
|
+
hasMultiDashboards: !!data.multiDashboards,
|
|
122
|
+
dashboardCount: data.multiDashboards?.length || 0,
|
|
123
|
+
activeDashboard: data.activeDashboard,
|
|
124
|
+
datasetCount: Object.keys(data.datasets || {}).length,
|
|
125
|
+
vizCount: data.visualizations ? Object.keys(data.visualizations).length : 0
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
setConfig(data)
|
|
129
|
+
})
|
|
130
|
+
.catch(err => {
|
|
131
|
+
console.error('Failed to fetch config:', configUrl, err)
|
|
132
|
+
})
|
|
133
|
+
}, [configUrl])
|
|
134
|
+
|
|
135
|
+
return config
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
type DashboardStory = StoryObj<typeof Dashboard>
|
|
139
|
+
|
|
140
|
+
// Helper function to test dashboard rendering
|
|
141
|
+
const testDashboardRendering = async (canvasElement: HTMLElement, storyName: string) => {
|
|
142
|
+
await step('Wait for dashboard to render', async () => {
|
|
143
|
+
await new Promise<void>((resolve, reject) => {
|
|
144
|
+
const startTime = Date.now()
|
|
145
|
+
const timeout = 30000 // Longer timeout for external data loading
|
|
146
|
+
|
|
147
|
+
const checkDashboard = () => {
|
|
148
|
+
const dashboardElement = canvasElement.querySelector('.type-dashboard')
|
|
149
|
+
const loadingDiv = canvasElement.querySelector('div')
|
|
150
|
+
|
|
151
|
+
// Log current state for debugging
|
|
152
|
+
if (!dashboardElement && loadingDiv?.textContent?.includes('Loading')) {
|
|
153
|
+
console.log('Still loading config...')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (dashboardElement) {
|
|
157
|
+
resolve()
|
|
158
|
+
} else if (Date.now() - startTime > timeout) {
|
|
159
|
+
reject(new Error(`Timeout: Dashboard element not found after ${timeout}ms`))
|
|
160
|
+
} else {
|
|
161
|
+
setTimeout(checkDashboard, 100)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
checkDashboard()
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
await step('Verify dashboard wrapper is present', async () => {
|
|
169
|
+
const dashboard = canvasElement.querySelector('.type-dashboard')
|
|
170
|
+
expect(dashboard).toBeInTheDocument()
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
await step('Verify at least one visualization rendered', async () => {
|
|
174
|
+
const coveModules = canvasElement.querySelectorAll('.cdc-open-viz-module')
|
|
175
|
+
expect(coveModules.length).toBeGreaterThan(0)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
console.log(` ${storyName} dashboard rendered successfully`)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Explore by Location Dashboard
|
|
183
|
+
*
|
|
184
|
+
* Interactive dashboard showing BRFSS prevalence data organized by geographic location.
|
|
185
|
+
* Users can explore health indicators by state, territory, and metropolitan/micropolitan
|
|
186
|
+
* statistical areas (MMSA).
|
|
187
|
+
*/
|
|
188
|
+
export const Explore_By_Location: DashboardStory = {
|
|
189
|
+
render: () => {
|
|
190
|
+
const config = useConfigWithAbsoluteDataUrl(CONFIG_URLS.exploreByLocation)
|
|
191
|
+
if (!config) return <div>Loading...</div>
|
|
192
|
+
return <Dashboard config={config} />
|
|
193
|
+
},
|
|
194
|
+
play: async ({ canvasElement }) => {
|
|
195
|
+
await testDashboardRendering(canvasElement, 'Explore By Location Dashboard')
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Explore by Topic Dashboard
|
|
201
|
+
*
|
|
202
|
+
* Interactive dashboard showing BRFSS prevalence data organized by health topics.
|
|
203
|
+
* Users can explore various health indicators and risk factors across different
|
|
204
|
+
* categories such as chronic diseases, health behaviors, and preventive practices.
|
|
205
|
+
*/
|
|
206
|
+
export const Explore_By_Topic: DashboardStory = {
|
|
207
|
+
render: () => {
|
|
208
|
+
const config = useConfigWithAbsoluteDataUrl(CONFIG_URLS.exploreByTopic)
|
|
209
|
+
if (!config) return <div>Loading...</div>
|
|
210
|
+
return <Dashboard config={config} />
|
|
211
|
+
},
|
|
212
|
+
play: async ({ canvasElement }) => {
|
|
213
|
+
await testDashboardRendering(canvasElement, 'Explore By Topic Dashboard')
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* All BRFSS Dashboards - Combined Test
|
|
219
|
+
*
|
|
220
|
+
* Tests both BRFSS dashboards to ensure they all render correctly together.
|
|
221
|
+
*/
|
|
222
|
+
export const All_BRFSS_Dashboards: StoryObj = {
|
|
223
|
+
render: () => {
|
|
224
|
+
const locationConfig = useConfigWithAbsoluteDataUrl(CONFIG_URLS.exploreByLocation)
|
|
225
|
+
const topicConfig = useConfigWithAbsoluteDataUrl(CONFIG_URLS.exploreByTopic)
|
|
226
|
+
|
|
227
|
+
if (!locationConfig || !topicConfig) {
|
|
228
|
+
return <div>Loading...</div>
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<div className='container-fluid p-4'>
|
|
233
|
+
<h1 className='mb-4'>BRFSS Prevalence Data - All Dashboards</h1>
|
|
234
|
+
|
|
235
|
+
<section className='mb-5'>
|
|
236
|
+
<h2>Explore by Location</h2>
|
|
237
|
+
<Dashboard config={locationConfig} />
|
|
238
|
+
</section>
|
|
239
|
+
|
|
240
|
+
<section className='mb-5'>
|
|
241
|
+
<h2>Explore by Topic</h2>
|
|
242
|
+
<Dashboard config={topicConfig} />
|
|
243
|
+
</section>
|
|
244
|
+
</div>
|
|
245
|
+
)
|
|
246
|
+
},
|
|
247
|
+
play: async ({ canvasElement }) => {
|
|
248
|
+
const canvas = within(canvasElement)
|
|
249
|
+
|
|
250
|
+
await step('Wait for all configs to load', async () => {
|
|
251
|
+
await new Promise<void>(resolve => {
|
|
252
|
+
const checkLoading = () => {
|
|
253
|
+
const loadingDiv = canvasElement.querySelector('div:not(.container-fluid)')
|
|
254
|
+
if (!loadingDiv || !loadingDiv.textContent?.includes('Loading')) {
|
|
255
|
+
resolve()
|
|
256
|
+
} else {
|
|
257
|
+
setTimeout(checkLoading, 100)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
checkLoading()
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
await step('Wait for visualizations to start rendering', async () => {
|
|
265
|
+
await new Promise<void>(resolve => setTimeout(resolve, 2000))
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
await step('Wait for both dashboards to render', async () => {
|
|
269
|
+
await new Promise<void>((resolve, reject) => {
|
|
270
|
+
const startTime = Date.now()
|
|
271
|
+
const timeout = 40000
|
|
272
|
+
|
|
273
|
+
const checkDashboards = () => {
|
|
274
|
+
const dashboards = canvasElement.querySelectorAll('.type-dashboard')
|
|
275
|
+
if (dashboards.length >= 2) {
|
|
276
|
+
resolve()
|
|
277
|
+
} else if (Date.now() - startTime > timeout) {
|
|
278
|
+
reject(new Error(`Timeout: Only ${dashboards.length}/2 dashboards found after ${timeout}ms`))
|
|
279
|
+
} else {
|
|
280
|
+
setTimeout(checkDashboards, 200)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
checkDashboards()
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
await step('Verify both dashboards are present', async () => {
|
|
288
|
+
const dashboards = canvasElement.querySelectorAll('.type-dashboard')
|
|
289
|
+
expect(dashboards.length).toBe(2)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
console.log(` All 2 BRFSS dashboards rendered successfully`)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { within, expect } from 'storybook/test'
|
|
3
|
+
import Dashboard from '@cdc/dashboard'
|
|
4
|
+
import { useEffect, useState } from 'react'
|
|
5
|
+
|
|
6
|
+
// Fallback step function for test descriptions
|
|
7
|
+
const step = async (description: string, fn: () => Promise<void> | void) => {
|
|
8
|
+
console.log(`▶ ${description}`)
|
|
9
|
+
await fn()
|
|
10
|
+
console.log(`✓ ${description}`)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const meta: Meta = {
|
|
14
|
+
title: 'Regression Tests/Pages/Cancer Registries',
|
|
15
|
+
parameters: {
|
|
16
|
+
layout: 'fullscreen',
|
|
17
|
+
docs: {
|
|
18
|
+
description: {
|
|
19
|
+
component:
|
|
20
|
+
'Stories for visualizations from the CDC National Program of Cancer Registries (NPCR) Contact page (https://www.cdc.gov/national-program-cancer-registries/contact/index.html)'
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
tags: ['autodocs']
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default meta
|
|
28
|
+
|
|
29
|
+
// Config URL from the cancer registries contact page
|
|
30
|
+
const CONFIG_URLS = {
|
|
31
|
+
npcrPrograms: 'https://www.cdc.gov/cancer/npcr/modules/npcr-programs.json'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Helper to fetch config and update data URLs to use absolute cdc.gov paths
|
|
35
|
+
const useConfigWithAbsoluteDataUrl = (configUrl: string) => {
|
|
36
|
+
const [config, setConfig] = useState(null)
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
fetch(configUrl)
|
|
40
|
+
.then(res => res.json())
|
|
41
|
+
.then(data => {
|
|
42
|
+
// Convert relative data URLs to absolute cdc.gov URLs
|
|
43
|
+
if (data.dataUrl) {
|
|
44
|
+
// Handle different relative path formats (../../path or /path)
|
|
45
|
+
const dataUrl = data.dataUrl.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
|
|
46
|
+
data.dataUrl = `https://www.cdc.gov/${dataUrl}`
|
|
47
|
+
}
|
|
48
|
+
if (data.dataFileName) {
|
|
49
|
+
const dataFileName = data.dataFileName.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
|
|
50
|
+
data.dataFileName = `https://www.cdc.gov/${dataFileName}`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// For dashboard configs with multiDashboards, convert dataKey references in visualizations
|
|
54
|
+
if (data.multiDashboards) {
|
|
55
|
+
data.multiDashboards.forEach((dashboard: any) => {
|
|
56
|
+
if (dashboard.visualizations) {
|
|
57
|
+
Object.values(dashboard.visualizations).forEach((viz: any) => {
|
|
58
|
+
// Only convert dataKey if it's a URL path (starts with / or ../)
|
|
59
|
+
if (viz.dataKey && (viz.dataKey.startsWith('/') || viz.dataKey.startsWith('../'))) {
|
|
60
|
+
const dataKey = viz.dataKey.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
|
|
61
|
+
viz.dataKey = `https://www.cdc.gov/${dataKey}`
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// For dashboard configs, convert dataKey references in visualizations
|
|
69
|
+
if (data.visualizations) {
|
|
70
|
+
Object.values(data.visualizations).forEach((viz: any) => {
|
|
71
|
+
// Only convert dataKey if it's a URL path (starts with / or ../)
|
|
72
|
+
if (viz.dataKey && (viz.dataKey.startsWith('/') || viz.dataKey.startsWith('../'))) {
|
|
73
|
+
const dataKey = viz.dataKey.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
|
|
74
|
+
viz.dataKey = `https://www.cdc.gov/${dataKey}`
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// For dashboard configs, convert datasets only if they reference external files
|
|
80
|
+
if (data.datasets) {
|
|
81
|
+
const newDatasets = {}
|
|
82
|
+
Object.entries(data.datasets).forEach(([key, dataset]: [string, any]) => {
|
|
83
|
+
// Check if dataset has embedded data
|
|
84
|
+
const hasEmbeddedData = (dataset as any).data && Array.isArray((dataset as any).data)
|
|
85
|
+
|
|
86
|
+
// If data is embedded, keep the original key
|
|
87
|
+
if (hasEmbeddedData) {
|
|
88
|
+
newDatasets[key] = dataset
|
|
89
|
+
} else {
|
|
90
|
+
// Otherwise, convert paths to absolute URLs (but keep absolute URLs as-is)
|
|
91
|
+
const newKey = key.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
|
|
92
|
+
const absoluteKey = key.startsWith('http') ? key : `https://www.cdc.gov/${newKey}`
|
|
93
|
+
|
|
94
|
+
newDatasets[absoluteKey] = {
|
|
95
|
+
...dataset,
|
|
96
|
+
dataFileName:
|
|
97
|
+
(dataset as any).dataFileName && !(dataset as any).dataFileName.startsWith('http')
|
|
98
|
+
? `https://www.cdc.gov/${(dataset as any).dataFileName
|
|
99
|
+
.replace(/^(\.\.\/)+/, '')
|
|
100
|
+
.replace(/^\//, '')}`
|
|
101
|
+
: (dataset as any).dataFileName,
|
|
102
|
+
dataUrl:
|
|
103
|
+
(dataset as any).dataUrl && !(dataset as any).dataUrl.startsWith('http')
|
|
104
|
+
? `https://www.cdc.gov/${(dataset as any).dataUrl.replace(/^(\.\.\/)+/, '').replace(/^\//, '')}`
|
|
105
|
+
: (dataset as any).dataUrl
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
data.datasets = newDatasets
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Set activeDashboard to 0 if it's null and multiDashboards exist
|
|
113
|
+
if (data.multiDashboards && data.multiDashboards.length > 0 && data.activeDashboard === null) {
|
|
114
|
+
data.activeDashboard = 0
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Log config info for debugging
|
|
118
|
+
console.log('✓ Config loaded:', {
|
|
119
|
+
type: data.type,
|
|
120
|
+
hasMultiDashboards: !!data.multiDashboards,
|
|
121
|
+
dashboardCount: data.multiDashboards?.length || 0,
|
|
122
|
+
datasetCount: Object.keys(data.datasets || {}).length,
|
|
123
|
+
vizCount: data.visualizations ? Object.keys(data.visualizations).length : 0,
|
|
124
|
+
vizTypes: data.visualizations ? Object.values(data.visualizations).map((v: any) => v.type) : []
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
setConfig(data)
|
|
128
|
+
})
|
|
129
|
+
.catch(err => {
|
|
130
|
+
console.error('Failed to fetch config:', configUrl, err)
|
|
131
|
+
})
|
|
132
|
+
}, [configUrl])
|
|
133
|
+
|
|
134
|
+
return config
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
type DashboardStory = StoryObj<typeof Dashboard>
|
|
138
|
+
|
|
139
|
+
// Helper function to test dashboard rendering
|
|
140
|
+
const testDashboardRendering = async (canvasElement: HTMLElement, storyName: string) => {
|
|
141
|
+
await step('Wait for dashboard to render', async () => {
|
|
142
|
+
await new Promise<void>((resolve, reject) => {
|
|
143
|
+
const startTime = Date.now()
|
|
144
|
+
const timeout = 20000
|
|
145
|
+
|
|
146
|
+
const checkDashboard = () => {
|
|
147
|
+
const dashboardElement = canvasElement.querySelector('.type-dashboard')
|
|
148
|
+
const loadingDiv = canvasElement.querySelector('div')
|
|
149
|
+
|
|
150
|
+
// Log current state for debugging
|
|
151
|
+
if (!dashboardElement && loadingDiv?.textContent?.includes('Loading')) {
|
|
152
|
+
console.log('Still loading config...')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (dashboardElement) {
|
|
156
|
+
resolve()
|
|
157
|
+
} else if (Date.now() - startTime > timeout) {
|
|
158
|
+
reject(new Error(`Timeout: Dashboard element not found after ${timeout}ms`))
|
|
159
|
+
} else {
|
|
160
|
+
setTimeout(checkDashboard, 100)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
checkDashboard()
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
await step('Verify dashboard wrapper is present', async () => {
|
|
168
|
+
const dashboard = canvasElement.querySelector('.type-dashboard')
|
|
169
|
+
expect(dashboard).toBeInTheDocument()
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
await step('Verify at least one visualization rendered', async () => {
|
|
173
|
+
const coveModules = canvasElement.querySelectorAll('.cdc-open-viz-module')
|
|
174
|
+
expect(coveModules.length).toBeGreaterThan(0)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
console.log(` ${storyName} dashboard rendered successfully`)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* NPCR Programs Contact Map
|
|
182
|
+
*
|
|
183
|
+
* Interactive navigation map showing National Program of Cancer Registries (NPCR) programs
|
|
184
|
+
* across the United States, territories, and U.S.-affiliated Pacific Islands. Users can
|
|
185
|
+
* click on states to access contact information for their local cancer registry program.
|
|
186
|
+
*
|
|
187
|
+
* The NPCR supports central cancer registries in all 50 states, the District of Columbia,
|
|
188
|
+
* Puerto Rico, the U.S. Pacific Island Jurisdictions, and the U.S. Virgin Islands.
|
|
189
|
+
*/
|
|
190
|
+
export const NPCR_Programs_Map: DashboardStory = {
|
|
191
|
+
render: () => {
|
|
192
|
+
const config = useConfigWithAbsoluteDataUrl(CONFIG_URLS.npcrPrograms)
|
|
193
|
+
if (!config) return <div>Loading...</div>
|
|
194
|
+
return <Dashboard config={config} />
|
|
195
|
+
},
|
|
196
|
+
play: async ({ canvasElement }) => {
|
|
197
|
+
await testDashboardRendering(canvasElement, 'NPCR Programs Map')
|
|
198
|
+
}
|
|
199
|
+
}
|