@cdc/core 4.26.1 → 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.
Files changed (99) hide show
  1. package/.claude/agents/qa-test-developer.md +126 -0
  2. package/CLAUDE.local.md +67 -0
  3. package/_stories/Gallery.Charts.stories.tsx +34 -41
  4. package/_stories/Gallery.DataBite.stories.tsx +14 -7
  5. package/_stories/Gallery.Maps.stories.tsx +36 -27
  6. package/_stories/Gallery.WaffleChart.stories.tsx +1 -1
  7. package/_stories/PageART.stories.tsx +4 -3
  8. package/_stories/PageBRFSS.stories.tsx +20 -15
  9. package/_stories/PageCancerRegistries.stories.tsx +14 -14
  10. package/_stories/PageEasternEquineEncephalitis.stories.tsx +30 -16
  11. package/_stories/PageExcessiveAlcoholUse.stories.tsx +148 -143
  12. package/_stories/PageMaternalMortality.stories.tsx +4 -3
  13. package/_stories/PageOralHealth.stories.tsx +14 -9
  14. package/_stories/PageSmokingTobacco.stories.tsx +14 -9
  15. package/_stories/PageStateDiabetesProfiles.stories.tsx +14 -9
  16. package/_stories/PageWastewater.stories.tsx +40 -26
  17. package/_stories/VegaImport.stories.tsx +401 -0
  18. package/_stories/vega-fixtures/bars-with-line.json +444 -0
  19. package/_stories/vega-fixtures/bars.json +58 -0
  20. package/_stories/vega-fixtures/combo-bar-rolling-mean.json +88 -0
  21. package/_stories/vega-fixtures/combo.json +68 -0
  22. package/_stories/vega-fixtures/grouped-horizontal-bars.json +83 -0
  23. package/_stories/vega-fixtures/grouped-horizontal-bars2.json +231 -0
  24. package/_stories/vega-fixtures/horizontal-bar.json +427 -0
  25. package/_stories/vega-fixtures/horizontal-bars-with-bad-colors.json +197 -0
  26. package/_stories/vega-fixtures/horizontal-bars2.json +58 -0
  27. package/_stories/vega-fixtures/lines.json +227 -0
  28. package/_stories/vega-fixtures/measles-bars.json +348 -0
  29. package/_stories/vega-fixtures/measles-map.json +11101 -0
  30. package/_stories/vega-fixtures/measles-stacked-bars.json +2147 -0
  31. package/_stories/vega-fixtures/multi-dataset.json +255 -0
  32. package/_stories/vega-fixtures/no-data.json +14 -0
  33. package/_stories/vega-fixtures/pie-chart.json +94 -0
  34. package/_stories/vega-fixtures/repeat-spec.json +47 -0
  35. package/_stories/vega-fixtures/stacked-area.json +222 -0
  36. package/_stories/vega-fixtures/stacked-bar-with-rect.json +3412 -0
  37. package/_stories/vega-fixtures/stacked-bars-with-line.json +364 -0
  38. package/_stories/vega-fixtures/stacked-bars.json +212 -0
  39. package/_stories/vega-fixtures/stacked-horizontal-bars.json +140 -0
  40. package/_stories/vega-fixtures/warning-combo.json +59 -0
  41. package/_stories/vega-fixtures/warning-scatter-and-line.json +1182 -0
  42. package/assets/icon-chart-area.svg +1 -0
  43. package/assets/icon-chart-radar.svg +23 -0
  44. package/assets/logo2.svg +31 -0
  45. package/components/AdvancedEditor/EmbedEditor.tsx +270 -38
  46. package/components/CustomColorsEditor/CustomColorsEditor.tsx +3 -10
  47. package/components/DataTable/helpers/getSeriesName.ts +6 -0
  48. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +14 -6
  49. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +4 -0
  50. package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +33 -29
  51. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +2 -2
  52. package/components/Layout/components/Visualization/index.tsx +11 -0
  53. package/components/MediaControls.tsx +0 -1
  54. package/components/_stories/CustomColorsEditor.stories.tsx +37 -0
  55. package/components/_stories/DataTable.stories.tsx +1 -0
  56. package/data/colorPalettes.ts +18 -5
  57. package/data/mapColorPalettes.ts +10 -0
  58. package/devTemplate/dev.js +235 -0
  59. package/devTemplate/index.html +30 -0
  60. package/devTemplate/preview.html +1503 -0
  61. package/devTemplate/sidebar.css +151 -0
  62. package/dist/cove-main.css +2803 -4471
  63. package/dist/cove-main.css.map +1 -1
  64. package/generateViteConfig.js +111 -2
  65. package/helpers/DataTransform.ts +1 -5
  66. package/helpers/cove/date.ts +33 -1
  67. package/helpers/cove/string.ts +29 -0
  68. package/helpers/coveUpdateWorker.ts +3 -1
  69. package/helpers/embed/embedCodeGenerator.ts +80 -0
  70. package/helpers/embed/embedHelper.js +158 -0
  71. package/helpers/embed/filterUtils.ts +121 -0
  72. package/helpers/embed/index.ts +21 -0
  73. package/helpers/embed/urlValidation.ts +119 -0
  74. package/helpers/filterVizData.ts +6 -1
  75. package/helpers/getFileExtension.ts +0 -6
  76. package/helpers/metrics/types.ts +3 -0
  77. package/helpers/palettes/colorDistributions.ts +1 -1
  78. package/helpers/palettes/utils.ts +12 -12
  79. package/helpers/parseCsvWithQuotes.ts +15 -14
  80. package/helpers/prepareScreenshot.ts +27 -7
  81. package/helpers/testing.ts +44 -0
  82. package/helpers/tests/DataTransform.test.ts +125 -0
  83. package/helpers/tests/date.test.ts +64 -0
  84. package/helpers/vegaConfig.ts +1 -1
  85. package/helpers/vegaConfigImport.ts +160 -0
  86. package/helpers/ver/4.26.1.ts +1 -1
  87. package/helpers/ver/4.26.2.ts +84 -0
  88. package/helpers/ver/tests/4.26.1.test.ts +105 -0
  89. package/helpers/ver/tests/4.26.2.test.ts +298 -0
  90. package/helpers/viewports.ts +2 -0
  91. package/package.json +27 -32
  92. package/styles/v2/components/editor.scss +9 -9
  93. package/styles/v2/utils/_grid.scss +8 -3
  94. package/types/Annotation.ts +10 -11
  95. package/types/General.ts +2 -0
  96. package/types/Palette.ts +21 -0
  97. package/types/Visualization.ts +6 -0
  98. package/_stories/StoryRenderingTests.stories.tsx +0 -164
  99. package/helpers/embedCodeGenerator.ts +0 -109
@@ -115,18 +115,32 @@ type MapStory = StoryObj<typeof CdcMap>
115
115
  type ChartStory = StoryObj<typeof Chart>
116
116
  type DashboardStory = StoryObj<typeof Dashboard>
117
117
 
118
- // Helper function to test map rendering
118
+ // Helper function to test map rendering (supports both SVG and canvas-based maps)
119
119
  const testMapRendering = async (canvasElement: HTMLElement, storyName: string) => {
120
- const canvas = within(canvasElement)
121
-
122
120
  await step('Wait for map to render', async () => {
123
- const mapElement = await canvas.findByRole('img', { hidden: true }, { timeout: 10000 })
124
- expect(mapElement).toBeInTheDocument()
121
+ await new Promise<void>((resolve, reject) => {
122
+ const startTime = Date.now()
123
+ const timeout = 15000
124
+
125
+ const checkMap = () => {
126
+ const svgMap = canvasElement.querySelector('svg[role="img"]')
127
+ const canvasMap = canvasElement.querySelector('canvas')
128
+ if (svgMap || canvasMap) {
129
+ resolve()
130
+ } else if (Date.now() - startTime > timeout) {
131
+ reject(new Error(`Timeout: No map element (svg or canvas) found after ${timeout}ms`))
132
+ } else {
133
+ setTimeout(checkMap, 100)
134
+ }
135
+ }
136
+ checkMap()
137
+ })
125
138
  })
126
139
 
127
- await step('Verify SVG element is present', async () => {
128
- const svgElement = canvasElement.querySelector('svg')
129
- expect(svgElement).toBeInTheDocument()
140
+ await step('Verify map visualization is present', async () => {
141
+ const svgMap = canvasElement.querySelector('svg[role="img"]')
142
+ const canvasMap = canvasElement.querySelector('canvas')
143
+ expect(svgMap || canvasMap).toBeTruthy()
130
144
  })
131
145
 
132
146
  await step('Verify COVE module wrapper is present', async () => {
@@ -167,7 +181,7 @@ const testDashboardRendering = async (canvasElement: HTMLElement, storyName: str
167
181
  const timeout = 15000
168
182
 
169
183
  const checkDashboard = () => {
170
- const dashboardElement = canvasElement.querySelector('.cove-dashboard')
184
+ const dashboardElement = canvasElement.querySelector('.type-dashboard')
171
185
  if (dashboardElement) {
172
186
  resolve()
173
187
  } else if (Date.now() - startTime > timeout) {
@@ -181,7 +195,7 @@ const testDashboardRendering = async (canvasElement: HTMLElement, storyName: str
181
195
  })
182
196
 
183
197
  await step('Verify dashboard wrapper is present', async () => {
184
- const dashboard = canvasElement.querySelector('.cove-dashboard')
198
+ const dashboard = canvasElement.querySelector('.type-dashboard')
185
199
  expect(dashboard).toBeInTheDocument()
186
200
  })
187
201
 
@@ -289,14 +303,14 @@ export const COVID_Time_Period_Map: MapStory = {
289
303
  *
290
304
  * COVID-19 wastewater data visualization by state.
291
305
  */
292
- export const COVID_State_Level: ChartStory = {
306
+ export const COVID_State_Level: MapStory = {
293
307
  render: () => {
294
308
  const config = useConfigWithAbsoluteDataUrl(CONFIG_URLS.covidStateLevel)
295
309
  if (!config) return <div>Loading...</div>
296
- return <Chart config={config} />
310
+ return <CdcMap config={config} />
297
311
  },
298
312
  play: async ({ canvasElement }) => {
299
- await testChartRendering(canvasElement, 'COVID State Level')
313
+ await testMapRendering(canvasElement, 'COVID State Level')
300
314
  }
301
315
  }
302
316
 
@@ -325,10 +339,10 @@ export const COVID_State_Level_Rest: ChartStory = {
325
339
  render: () => {
326
340
  const config = useConfigWithAbsoluteDataUrl(CONFIG_URLS.covidStateLevelRest)
327
341
  if (!config) return <div>Loading...</div>
328
- return <Chart config={config} />
342
+ return <Dashboard config={config} />
329
343
  },
330
344
  play: async ({ canvasElement }) => {
331
- await testChartRendering(canvasElement, 'COVID State Level Rest')
345
+ await testDashboardRendering(canvasElement, 'COVID State Level Rest')
332
346
  }
333
347
  }
334
348
 
@@ -365,50 +379,50 @@ export const All_Wastewater_Visualizations: StoryObj = {
365
379
  }
366
380
 
367
381
  return (
368
- <div className="container-fluid p-4">
369
- <h1 className="mb-4">NWSS - All Wastewater Visualizations</h1>
382
+ <div className='container-fluid p-4'>
383
+ <h1 className='mb-4'>NWSS - All Wastewater Visualizations</h1>
370
384
 
371
- <section className="mb-5">
385
+ <section className='mb-5'>
372
386
  <h2>NWSS Home Page</h2>
373
387
  <Dashboard config={homePageConfig} />
374
388
  </section>
375
389
 
376
- <section className="mb-5">
390
+ <section className='mb-5'>
377
391
  <h2>Measles - Summary Modules</h2>
378
392
  <Dashboard config={measlesTopConfig} />
379
393
  </section>
380
394
 
381
- <section className="mb-5">
395
+ <section className='mb-5'>
382
396
  <h2>Measles - US Map</h2>
383
397
  <CdcMap config={measlesMapConfig} />
384
398
  </section>
385
399
 
386
- <section className="mb-5">
400
+ <section className='mb-5'>
387
401
  <h2>Measles - Time Period</h2>
388
402
  <Dashboard config={measlesTimePeriodConfig} />
389
403
  </section>
390
404
 
391
- <section className="mb-5">
405
+ <section className='mb-5'>
392
406
  <h2>COVID-19 - Summary Modules</h2>
393
407
  <Dashboard config={covidTopConfig} />
394
408
  </section>
395
409
 
396
- <section className="mb-5">
410
+ <section className='mb-5'>
397
411
  <h2>COVID-19 - State Map</h2>
398
412
  <CdcMap config={covidMapConfig} />
399
413
  </section>
400
414
 
401
- <section className="mb-5">
415
+ <section className='mb-5'>
402
416
  <h2>COVID-19 - State Level Data</h2>
403
417
  <Chart config={covidStateLevelConfig} />
404
418
  </section>
405
419
 
406
- <section className="mb-5">
420
+ <section className='mb-5'>
407
421
  <h2>COVID-19 - National and Regional Trends</h2>
408
422
  <Chart config={covidNationalRegionalConfig} />
409
423
  </section>
410
424
 
411
- <section className="mb-5">
425
+ <section className='mb-5'>
412
426
  <h2>COVID-19 - State Trends</h2>
413
427
  <Chart config={covidStateRestConfig} />
414
428
  </section>
@@ -0,0 +1,401 @@
1
+ /**
2
+ * Vega Import Stories
3
+ *
4
+ * Each story imports a Vega/Vega-Lite JSON config, converts it to a COVE config
5
+ * via the Vega importer pipeline, and renders the resulting chart or map.
6
+ *
7
+ * Play functions verify that each conversion renders without errors.
8
+ */
9
+ import React from 'react'
10
+ import type { Meta, StoryObj } from '@storybook/react-vite'
11
+ import { within, expect, waitFor } from 'storybook/test'
12
+
13
+ import Chart from '@cdc/chart/src/CdcChart'
14
+ import CdcMap from '@cdc/map/src/CdcMap'
15
+ import { maybeConvertVega } from '@cdc/core/helpers/vegaConfigImport'
16
+
17
+ // --- Import Vega fixture JSON files (alphabetical by filename) ---
18
+ import vegaBarsWithLine from './vega-fixtures/bars-with-line.json'
19
+ import vegaBars from './vega-fixtures/bars.json'
20
+ import vegaComboBarRollingMean from './vega-fixtures/combo-bar-rolling-mean.json'
21
+ import vegaCombo from './vega-fixtures/combo.json'
22
+ import vegaGroupedHorizontalBars from './vega-fixtures/grouped-horizontal-bars.json'
23
+ import vegaGroupedHorizontalBars2 from './vega-fixtures/grouped-horizontal-bars2.json'
24
+ import vegaHorizontalBar from './vega-fixtures/horizontal-bar.json'
25
+ import vegaHorizontalBarsWithBadColors from './vega-fixtures/horizontal-bars-with-bad-colors.json'
26
+ import vegaHorizontalBars2 from './vega-fixtures/horizontal-bars2.json'
27
+ import vegaLines from './vega-fixtures/lines.json'
28
+ import vegaMeaslesBars from './vega-fixtures/measles-bars.json'
29
+ import vegaMeaslesMap from './vega-fixtures/measles-map.json'
30
+ import vegaMeaslesStackedBars from './vega-fixtures/measles-stacked-bars.json'
31
+ import vegaMultiDataset from './vega-fixtures/multi-dataset.json'
32
+ import vegaNoData from './vega-fixtures/no-data.json'
33
+ import vegaPieChart from './vega-fixtures/pie-chart.json'
34
+ import vegaRepeatSpec from './vega-fixtures/repeat-spec.json'
35
+ import vegaStackedArea from './vega-fixtures/stacked-area.json'
36
+ import vegaStackedBarWithRect from './vega-fixtures/stacked-bar-with-rect.json'
37
+ import vegaStackedBars from './vega-fixtures/stacked-bars.json'
38
+ import vegaStackedBarsWithLine from './vega-fixtures/stacked-bars-with-line.json'
39
+ import vegaStackedHorizontalBars from './vega-fixtures/stacked-horizontal-bars.json'
40
+ import vegaWarningCombo from './vega-fixtures/warning-combo.json'
41
+ import vegaWarningScatterAndLine from './vega-fixtures/warning-scatter-and-line.json'
42
+
43
+ // --- Convert all configs at module evaluation time (top-level await) ---
44
+ // Ordered alphabetically by original filename
45
+ const fixtures: Record<string, any> = {
46
+ barsWithLine: vegaBarsWithLine,
47
+ bars: vegaBars,
48
+ comboBarRollingMean: vegaComboBarRollingMean,
49
+ combo: vegaCombo,
50
+ groupedHorizontalBars: vegaGroupedHorizontalBars,
51
+ groupedHorizontalBars2: vegaGroupedHorizontalBars2,
52
+ horizontalBar: vegaHorizontalBar,
53
+ horizontalBarsWithBadColors: vegaHorizontalBarsWithBadColors,
54
+ horizontalBars2: vegaHorizontalBars2,
55
+ lines: vegaLines,
56
+ measlesBars: vegaMeaslesBars,
57
+ measlesMap: vegaMeaslesMap,
58
+ measlesStackedBars: vegaMeaslesStackedBars,
59
+ multiDataset: vegaMultiDataset,
60
+ noData: vegaNoData,
61
+ pieChart: vegaPieChart,
62
+ repeatSpec: vegaRepeatSpec,
63
+ stackedArea: vegaStackedArea,
64
+ stackedBarWithRect: vegaStackedBarWithRect,
65
+ stackedBars: vegaStackedBars,
66
+ stackedBarsWithLine: vegaStackedBarsWithLine,
67
+ stackedHorizontalBars: vegaStackedHorizontalBars,
68
+ warningCombo: vegaWarningCombo,
69
+ warningScatterAndLine: vegaWarningScatterAndLine
70
+ }
71
+
72
+ const convertedConfigs: Record<string, any> = {}
73
+ const conversionErrors: Record<string, string[]> = {}
74
+ const conversionWarnings: Record<string, string[]> = {}
75
+
76
+ for (const [key, vegaJson] of Object.entries(fixtures)) {
77
+ // Capture console warnings/errors during conversion
78
+ const warnings: string[] = []
79
+ const errors: string[] = []
80
+
81
+ const originalWarn = console.warn
82
+ const originalError = console.error
83
+
84
+ console.warn = (...args: any[]) => {
85
+ const message = args.map(arg => (typeof arg === 'string' ? arg : JSON.stringify(arg))).join(' ')
86
+ warnings.push(message)
87
+ }
88
+
89
+ console.error = (...args: any[]) => {
90
+ const message = args.map(arg => (typeof arg === 'string' ? arg : JSON.stringify(arg))).join(' ')
91
+ errors.push(message)
92
+ }
93
+
94
+ try {
95
+ convertedConfigs[key] = await maybeConvertVega({ ...vegaJson })
96
+ } catch (err) {
97
+ console.error(`Failed to convert ${key}:`, err)
98
+ convertedConfigs[key] = null
99
+ }
100
+
101
+ console.warn = originalWarn
102
+ console.error = originalError
103
+
104
+ conversionErrors[key] = [...warnings, ...errors]
105
+ conversionWarnings[key] = warnings
106
+ }
107
+
108
+ // --- Storybook meta ---
109
+ const meta: Meta = {
110
+ title: 'Regression Tests/Vega Importer',
111
+ parameters: {
112
+ layout: 'fullscreen'
113
+ },
114
+ tags: ['!autodocs']
115
+ }
116
+
117
+ export default meta
118
+
119
+ // --- Shared play function: verify the chart/map rendered ---
120
+ const assertRendered = async ({ canvasElement }: { canvasElement: HTMLElement }) => {
121
+ await waitFor(
122
+ () => {
123
+ const coveModule = canvasElement.querySelector('.cdc-open-viz-module')
124
+ expect(coveModule).toBeTruthy()
125
+ },
126
+ { timeout: 15000 }
127
+ )
128
+
129
+ // Verify an SVG was rendered
130
+ const svgs = canvasElement.querySelectorAll('svg')
131
+ expect(svgs.length).toBeGreaterThan(0)
132
+ }
133
+
134
+ // --- Shared play function: verify map rendered ---
135
+ const assertMapRendered = async ({ canvasElement }: { canvasElement: HTMLElement }) => {
136
+ await waitFor(
137
+ () => {
138
+ const coveModule = canvasElement.querySelector('.cdc-open-viz-module')
139
+ expect(coveModule).toBeTruthy()
140
+ },
141
+ { timeout: 15000 }
142
+ )
143
+
144
+ // For maps, check for either SVG or canvas elements
145
+ await waitFor(
146
+ () => {
147
+ const svgs = canvasElement.querySelectorAll('svg')
148
+ const canvases = canvasElement.querySelectorAll('canvas')
149
+ const hasRenderedContent = svgs.length > 0 || canvases.length > 0
150
+ expect(hasRenderedContent).toBe(true)
151
+ },
152
+ { timeout: 15000 }
153
+ )
154
+ }
155
+
156
+ // --- Shared play function: verify error configs failed properly ---
157
+ const assertConversionFailed =
158
+ (expectedErrorPattern: string | RegExp) =>
159
+ async ({ canvasElement }: { canvasElement: HTMLElement }) => {
160
+ // Wait a bit for any async operations to complete
161
+ await new Promise(resolve => setTimeout(resolve, 500))
162
+
163
+ // Verify error message is displayed
164
+ const errorElement = canvasElement.querySelector('[data-testid="error"]')
165
+ expect(errorElement).toBeTruthy()
166
+ expect(errorElement?.textContent).toContain('failed to convert')
167
+
168
+ // Verify the specific error message appears
169
+ const errorMessages = canvasElement.querySelector('[data-testid="error-messages"]')
170
+ expect(errorMessages).toBeTruthy()
171
+
172
+ const errorText = errorMessages?.textContent || ''
173
+ if (typeof expectedErrorPattern === 'string') {
174
+ expect(errorText).toContain(expectedErrorPattern)
175
+ } else {
176
+ expect(expectedErrorPattern.test(errorText)).toBe(true)
177
+ }
178
+
179
+ // Verify no visualization rendered
180
+ const coveModule = canvasElement.querySelector('.cdc-open-viz-module')
181
+ expect(coveModule).toBeNull()
182
+ }
183
+
184
+ // --- Helper to build a chart story ---
185
+ const chartStory = (key: string): StoryObj => ({
186
+ render: () => {
187
+ const config = convertedConfigs[key]
188
+ if (!config) return <div data-testid='error'>Config "{key}" failed to convert</div>
189
+ return <Chart config={{ ...config }} isEditor={false} />
190
+ },
191
+ play: assertRendered
192
+ })
193
+
194
+ // --- Helper to build a map story ---
195
+ const mapStory = (key: string): StoryObj => ({
196
+ render: () => {
197
+ const config = convertedConfigs[key]
198
+ if (!config) return <div data-testid='error'>Config "{key}" failed to convert</div>
199
+ return <CdcMap config={{ ...config }} navigationHandler={() => {}} setConfig={() => {}} />
200
+ },
201
+ play: assertMapRendered
202
+ })
203
+
204
+ // --- Helper to build an error story (expects conversion to fail) ---
205
+ const errorStory = (key: string, expectedErrorPattern: string | RegExp): StoryObj => ({
206
+ render: () => {
207
+ const config = convertedConfigs[key]
208
+ const errorMessages = conversionErrors[key] || []
209
+
210
+ if (!config) {
211
+ return (
212
+ <div style={{ padding: '20px', fontFamily: 'monospace' }}>
213
+ <div data-testid='error' style={{ color: '#28a745', fontWeight: 'bold', marginBottom: '10px' }}>
214
+ ✓ Config "{key}" failed to convert (expected)
215
+ </div>
216
+ <div
217
+ data-testid='error-messages'
218
+ style={{
219
+ backgroundColor: '#f5f5f5',
220
+ padding: '15px',
221
+ borderRadius: '4px',
222
+ border: '1px solid #ddd'
223
+ }}
224
+ >
225
+ <strong>Error Messages:</strong>
226
+ <ul style={{ marginTop: '10px', paddingLeft: '20px' }}>
227
+ {errorMessages.map((msg, i) => (
228
+ <li key={i} style={{ marginBottom: '5px' }}>
229
+ {msg}
230
+ </li>
231
+ ))}
232
+ </ul>
233
+ </div>
234
+ </div>
235
+ )
236
+ }
237
+
238
+ return (
239
+ <div data-testid='unexpected-success' style={{ padding: '20px', color: '#c41e3a', fontWeight: 'bold' }}>
240
+ ⚠️ Config "{key}" unexpectedly succeeded - this should have failed!
241
+ </div>
242
+ )
243
+ },
244
+ play: assertConversionFailed(expectedErrorPattern)
245
+ })
246
+
247
+ // --- Helper to build a warning story (renders with warnings) ---
248
+ const warningStory = (
249
+ key: string,
250
+ Component: typeof Chart | typeof CdcMap,
251
+ expectedWarningPattern: string | RegExp
252
+ ): StoryObj => ({
253
+ render: () => {
254
+ const config = convertedConfigs[key]
255
+ const warnings = conversionWarnings[key] || []
256
+
257
+ if (!config) {
258
+ return <div data-testid='error'>Config "{key}" failed to convert</div>
259
+ }
260
+
261
+ return (
262
+ <div>
263
+ {warnings.length > 0 && (
264
+ <div
265
+ data-testid='warnings'
266
+ style={{
267
+ backgroundColor: '#fff3cd',
268
+ border: '1px solid #ffc107',
269
+ borderRadius: '4px',
270
+ padding: '15px',
271
+ margin: '20px',
272
+ fontFamily: 'monospace',
273
+ fontSize: '14px'
274
+ }}
275
+ >
276
+ <div style={{ color: '#856404', fontWeight: 'bold', marginBottom: '10px' }}>⚠️ Conversion Warnings:</div>
277
+ <ul style={{ margin: '0', paddingLeft: '20px', color: '#856404' }}>
278
+ {warnings.map((msg, i) => (
279
+ <li key={i} style={{ marginBottom: '5px' }}>
280
+ {msg}
281
+ </li>
282
+ ))}
283
+ </ul>
284
+ </div>
285
+ )}
286
+ <Component config={{ ...config }} isEditor={false} />
287
+ </div>
288
+ )
289
+ },
290
+ play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
291
+ // Verify warnings were captured
292
+ const warningsElement = canvasElement.querySelector('[data-testid="warnings"]')
293
+ expect(warningsElement).toBeTruthy()
294
+
295
+ // Verify the warning contains the expected text
296
+ const warningText = warningsElement?.textContent || ''
297
+
298
+ if (typeof expectedWarningPattern === 'string') {
299
+ expect(warningText).toContain(expectedWarningPattern)
300
+ } else {
301
+ expect(expectedWarningPattern.test(warningText)).toBe(true)
302
+ }
303
+
304
+ // Verify the visualization still rendered
305
+ await assertRendered({ canvasElement })
306
+ }
307
+ })
308
+
309
+ // ============================================================================
310
+ // Stories — alphabetical by original filename
311
+ // ============================================================================
312
+
313
+ // bars-with-line.json
314
+ export const BarsWithLineTests: StoryObj = chartStory('barsWithLine')
315
+
316
+ // bars.json
317
+ export const BarsTests: StoryObj = chartStory('bars')
318
+
319
+ // combo-bar-rolling-mean.json
320
+ export const ComboBarRollingMeanTests: StoryObj = chartStory('comboBarRollingMean')
321
+
322
+ // combo.json
323
+ export const ComboTests: StoryObj = chartStory('combo')
324
+
325
+ // grouped-horizontal-bars.json
326
+ export const GroupedHorizontalBarsTests: StoryObj = chartStory('groupedHorizontalBars')
327
+
328
+ // grouped-horizontal-bars2.json
329
+ export const GroupedHorizontalBars2Tests: StoryObj = chartStory('groupedHorizontalBars2')
330
+
331
+ // horizontal-bar.json
332
+ export const HorizontalBarTests: StoryObj = chartStory('horizontalBar')
333
+
334
+ // horizontal-bars-with-bad-colors.json
335
+ export const HorizontalBarsWithBadColorsTests: StoryObj = chartStory('horizontalBarsWithBadColors')
336
+
337
+ // horizontal-bars2.json
338
+ export const HorizontalBars2Tests: StoryObj = chartStory('horizontalBars2')
339
+
340
+ // lines.json
341
+ export const LinesTests: StoryObj = chartStory('lines')
342
+
343
+ // measles-bars.json
344
+ export const MeaslesBarsTests: StoryObj = chartStory('measlesBars')
345
+
346
+ // measles-map.json
347
+ export const MeaslesMapTests: StoryObj = mapStory('measlesMap')
348
+
349
+ // measles-stacked-bars.json
350
+ export const MeaslesStackedBarsTests: StoryObj = chartStory('measlesStackedBars')
351
+
352
+ // multi-dataset.json
353
+ export const MultiDatasetTests: StoryObj = chartStory('multiDataset')
354
+
355
+ // stacked-area.json
356
+ export const StackedAreaTests: StoryObj = chartStory('stackedArea')
357
+
358
+ // stacked-bar-with-rect.json
359
+ export const StackedBarWithRectTests: StoryObj = chartStory('stackedBarWithRect')
360
+
361
+ // stacked-bars.json
362
+ export const StackedBarsTests: StoryObj = chartStory('stackedBars')
363
+
364
+ // stacked-horizontal-bars.json
365
+ export const StackedHorizontalBarsTests: StoryObj = chartStory('stackedHorizontalBars')
366
+
367
+ // ============================================================================
368
+ // Error Test Stories (Expected to Fail)
369
+ // ============================================================================
370
+
371
+ // no-data.json - Config references external data URL instead of embedded data
372
+ export const NoDataErrorTests: StoryObj = errorStory('noData', 'No data was found')
373
+
374
+ // pie-chart.json - Pie charts are not supported by COVE's Vega importer
375
+ export const PieChartErrorTests: StoryObj = errorStory(
376
+ 'pieChart',
377
+ /could not find a COVE chart type|Supported marks are/
378
+ )
379
+
380
+ // repeat-spec.json - Vega-Lite repeat/spec operators are not supported
381
+ export const RepeatSpecErrorTests: StoryObj = errorStory('repeatSpec', /does not support.*repeat\/spec operator/)
382
+
383
+ // stacked-bars-with-line.json - Complex combinations may not be supported
384
+ export const StackedBarsWithLineErrorTests: StoryObj = errorStory(
385
+ 'stackedBarsWithLine',
386
+ /one of them appears to be stacked/
387
+ )
388
+
389
+ // ============================================================================
390
+ // Warning Test Stories (Render with Warnings)
391
+ // ============================================================================
392
+
393
+ // warning-combo.json - Combo chart with multiple mark types
394
+ export const WarningComboTests: StoryObj = warningStory('warningCombo', Chart, 'only support these types of marks')
395
+
396
+ // warning-scatter-and-line.json - Scatter plot with line
397
+ export const WarningScatterAndLineTests: StoryObj = warningStory(
398
+ 'warningScatterAndLine',
399
+ Chart,
400
+ 'only support these types of marks'
401
+ )