@cdc/core 4.25.10 → 4.26.1

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 (134) hide show
  1. package/_stories/Gallery.Charts.stories.tsx +307 -0
  2. package/_stories/Gallery.DataBite.stories.tsx +72 -0
  3. package/_stories/Gallery.Maps.stories.tsx +230 -0
  4. package/_stories/Gallery.WaffleChart.stories.tsx +187 -0
  5. package/_stories/PageART.stories.tsx +192 -0
  6. package/_stories/PageBRFSS.stories.tsx +289 -0
  7. package/_stories/PageCancerRegistries.stories.tsx +199 -0
  8. package/_stories/PageEasternEquineEncephalitis.stories.tsx +202 -0
  9. package/_stories/PageExcessiveAlcoholUse.stories.tsx +196 -0
  10. package/_stories/PageMaternalMortality.stories.tsx +192 -0
  11. package/_stories/PageOralHealth.stories.tsx +196 -0
  12. package/_stories/PageRespiratory.stories.tsx +332 -0
  13. package/_stories/PageSmokingTobacco.stories.tsx +195 -0
  14. package/_stories/PageStateDiabetesProfiles.stories.tsx +196 -0
  15. package/_stories/PageWastewater.stories.tsx +463 -0
  16. package/_stories/StoryRenderingTests.stories.tsx +164 -0
  17. package/assets/icon-magnifying-glass.svg +5 -0
  18. package/assets/icon-warming-stripes.svg +13 -0
  19. package/components/AdvancedEditor/AdvancedEditor.tsx +7 -1
  20. package/components/AdvancedEditor/EmbedEditor.tsx +281 -0
  21. package/components/ComboBox/ComboBox.tsx +345 -0
  22. package/components/ComboBox/combobox.styles.css +185 -0
  23. package/components/ComboBox/index.ts +1 -0
  24. package/components/CustomColorsEditor/CustomColorsEditor.css +299 -0
  25. package/components/CustomColorsEditor/CustomColorsEditor.tsx +209 -0
  26. package/components/CustomColorsEditor/index.ts +1 -0
  27. package/components/DataTable/DataTable.tsx +132 -58
  28. package/components/DataTable/DataTableStandAlone.tsx +8 -3
  29. package/components/DataTable/components/DataTableEditorPanel.tsx +12 -2
  30. package/components/DataTable/data-table.css +217 -210
  31. package/components/DataTable/helpers/mapCellMatrix.tsx +28 -9
  32. package/components/DataTable/helpers/standardizeState.js +2 -2
  33. package/components/DataTable/helpers/tests/standardizeState.test.js +54 -0
  34. package/components/EditorPanel/ColumnsEditor.tsx +37 -19
  35. package/components/EditorPanel/DataTableEditor.tsx +54 -28
  36. package/components/EditorPanel/EditorPanel.styles.css +439 -0
  37. package/components/EditorPanel/EditorPanel.tsx +144 -0
  38. package/components/EditorPanel/EditorPanelDispatch.tsx +75 -0
  39. package/components/EditorPanel/FieldSetWrapper.tsx +66 -23
  40. package/components/EditorPanel/FootnotesEditor.tsx +44 -37
  41. package/components/EditorPanel/Inputs.tsx +44 -8
  42. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +35 -62
  43. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +246 -175
  44. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +61 -22
  45. package/components/EditorPanel/sections/VisualSection.tsx +169 -0
  46. package/components/Filters/Filters.tsx +57 -10
  47. package/components/Filters/components/Dropdown.tsx +6 -1
  48. package/components/Filters/helpers/getNestedOptions.ts +2 -1
  49. package/components/Filters/helpers/handleSorting.ts +1 -1
  50. package/components/Footnotes/Footnotes.tsx +35 -25
  51. package/components/Footnotes/FootnotesStandAlone.tsx +42 -6
  52. package/components/HeaderThemeSelector/HeaderThemeSelector.css +43 -0
  53. package/components/HeaderThemeSelector/HeaderThemeSelector.stories.tsx +74 -0
  54. package/components/HeaderThemeSelector/HeaderThemeSelector.tsx +61 -0
  55. package/components/HeaderThemeSelector/index.ts +2 -0
  56. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +82 -0
  57. package/components/Layout/components/Visualization/index.tsx +16 -1
  58. package/components/Layout/components/Visualization/visualizations.scss +7 -0
  59. package/components/Layout/styles/editor.scss +2 -1
  60. package/components/Legend/Legend.Gradient.tsx +1 -1
  61. package/components/Loader/Loader.tsx +1 -1
  62. package/components/MediaControls.tsx +63 -34
  63. package/components/PaletteConversionModal.tsx +7 -4
  64. package/components/PaletteSelector/PaletteSelector.css +49 -6
  65. package/components/Table/components/Cell.tsx +23 -2
  66. package/components/Table/components/Row.tsx +5 -3
  67. package/components/_stories/Filters.stories.tsx +20 -1
  68. package/components/_stories/Footnotes.CSV.stories.tsx +247 -0
  69. package/components/_stories/Footnotes.stories.tsx +768 -3
  70. package/components/_stories/Inputs.stories.tsx +2 -2
  71. package/components/_stories/styles.scss +0 -1
  72. package/components/ui/Accordion.jsx +1 -1
  73. package/components/ui/Icon.tsx +3 -1
  74. package/components/ui/Title/index.tsx +30 -2
  75. package/components/ui/Title/title.styles.css +42 -0
  76. package/components/ui/accordion.styles.css +57 -0
  77. package/data/chartColorPalettes.ts +1 -1
  78. package/dist/cove-main.css +75 -6
  79. package/dist/cove-main.css.map +1 -1
  80. package/generateViteConfig.js +8 -1
  81. package/helpers/addValuesToFilters.ts +11 -1
  82. package/helpers/constants.ts +37 -0
  83. package/helpers/cove/number.ts +33 -12
  84. package/helpers/coveUpdateWorker.ts +20 -11
  85. package/helpers/embedCodeGenerator.ts +109 -0
  86. package/helpers/fetchRemoteData.ts +3 -15
  87. package/helpers/getUniqueValues.ts +19 -0
  88. package/helpers/hashObj.ts +25 -0
  89. package/helpers/isRightAlignedTableValue.js +5 -0
  90. package/helpers/markupProcessor.ts +27 -12
  91. package/helpers/mergeCustomOrderValues.ts +37 -0
  92. package/helpers/metrics/helpers.ts +1 -0
  93. package/helpers/parseCsvWithQuotes.ts +65 -0
  94. package/helpers/pivotData.ts +2 -2
  95. package/helpers/prepareScreenshot.ts +268 -0
  96. package/helpers/queryStringUtils.ts +29 -0
  97. package/helpers/testing.ts +17 -4
  98. package/helpers/tests/prepareScreenshot.test.ts +414 -0
  99. package/helpers/tests/queryStringUtils.test.ts +381 -0
  100. package/helpers/tests/testStandaloneBuild.ts +23 -5
  101. package/helpers/useDataVizClasses.ts +0 -1
  102. package/helpers/ver/4.25.11.ts +13 -0
  103. package/helpers/ver/4.26.1.ts +80 -0
  104. package/helpers/viewports.ts +2 -0
  105. package/hooks/useDataColumns.ts +63 -0
  106. package/hooks/useFilterManagement.ts +94 -0
  107. package/hooks/useLegendSeparators.ts +26 -0
  108. package/hooks/useListManagement.ts +192 -0
  109. package/package.json +6 -4
  110. package/styles/_button-section.scss +0 -3
  111. package/styles/_common-components.css +73 -0
  112. package/styles/_global.scss +25 -5
  113. package/styles/base.scss +0 -50
  114. package/styles/cove-main.scss +3 -1
  115. package/styles/filters.scss +10 -3
  116. package/styles/v2/base/index.scss +0 -1
  117. package/styles/v2/components/editor.scss +14 -6
  118. package/styles/v2/utils/_breakpoints.scss +1 -1
  119. package/styles/v2/utils/index.scss +0 -1
  120. package/styles/waiting.scss +1 -1
  121. package/types/Axis.ts +1 -0
  122. package/types/ForecastingSeriesKey.ts +1 -0
  123. package/types/MarkupInclude.ts +5 -3
  124. package/types/MarkupVariable.ts +1 -1
  125. package/types/Series.ts +3 -0
  126. package/types/Table.ts +1 -0
  127. package/types/Visualization.ts +1 -0
  128. package/types/VizFilter.ts +2 -0
  129. package/LICENSE +0 -201
  130. package/styles/_mixins.scss +0 -13
  131. package/styles/_typography.scss +0 -0
  132. package/styles/v2/base/_typography.scss +0 -0
  133. package/styles/v2/components/guidance-block.scss +0 -74
  134. package/styles/v2/utils/_functions.scss +0 -0
@@ -13,6 +13,35 @@ export function getQueryStringFilterValue(filter) {
13
13
  }
14
14
  }
15
15
 
16
+ /**
17
+ * Check if a filter should be hidden based on URL query parameters
18
+ * Checks for hide{setByQueryParameter}=true|1|yes in the URL
19
+ *
20
+ * @param filter - Filter object with setByQueryParameter property
21
+ * @returns true if filter should be hidden, false otherwise
22
+ *
23
+ * @example
24
+ * // If filter has setByQueryParameter: "state"
25
+ * // URL: ?hidestate=true -> returns true
26
+ * // URL: ?hideState=true -> returns false (case sensitive!)
27
+ */
28
+ export function isFilterHiddenByQuery(filter) {
29
+ if (!filter) return false
30
+
31
+ // Only check setByQueryParameter - this is the standard way filters are identified in URL params
32
+ const paramName = filter.setByQueryParameter
33
+ if (!paramName) return false
34
+
35
+ const urlParams = new URLSearchParams(window.location.search)
36
+ const hideParamName = `hide${paramName}`
37
+ const hideValue = urlParams.get(hideParamName)
38
+
39
+ if (!hideValue) return false
40
+
41
+ const lower = hideValue.toLowerCase()
42
+ return lower === 'true' || hideValue === '1' || lower === 'yes'
43
+ }
44
+
16
45
  export function getQueryParams() {
17
46
  const queryParams = {}
18
47
  for (const [key, value] of Array.from(new URLSearchParams(window.location.search).entries())) {
@@ -39,7 +39,7 @@ export const MIN_ANIMATION_DELAY_MS = (() => {
39
39
  return 500
40
40
  })()
41
41
 
42
- const WAIT_FOR_TIMEOUT_MS = 5000
42
+ const WAIT_FOR_TIMEOUT_MS = 10000
43
43
 
44
44
  // ============================================================================
45
45
  // CORE POLLING UTILITIES
@@ -177,8 +177,11 @@ export const waitForTextContent = async (el: HTMLElement | null, expected: strin
177
177
  */
178
178
  export const waitForEditor = async (canvas: any) => {
179
179
  await waitForWithDelay(() => {
180
- const editorElement = canvas.queryAllByText(/general|data|visual/i)
181
- expect(editorElement[0]).toBeVisible()
180
+ const accordionButtons = canvas.getAllByRole('button', { name: /general|data|visual/i })
181
+ expect(accordionButtons.length).toBeGreaterThan(0)
182
+ for (const button of accordionButtons) {
183
+ expect(button).toBeVisible()
184
+ }
182
185
  })
183
186
  }
184
187
 
@@ -189,7 +192,17 @@ export const waitForEditor = async (canvas: any) => {
189
192
  * @param sectionName Name of the accordion section (case-insensitive)
190
193
  */
191
194
  export const openAccordion = async (canvas: any, sectionName: string) => {
192
- const accordion = canvas.getByRole('button', { name: new RegExp(sectionName, 'i') })
195
+ // Get all buttons with matching name and filter to only accordion buttons
196
+ const allButtons = canvas.getAllByRole('button', { name: new RegExp(sectionName, 'i') })
197
+ const accordion = allButtons.find(
198
+ (button: HTMLElement) =>
199
+ button.classList.contains('accordion__button') || button.closest('.editor-panel, .accordion')
200
+ )
201
+
202
+ if (!accordion) {
203
+ throw new Error(`Could not find accordion button for "${sectionName}"`)
204
+ }
205
+
193
206
  await userEvent.click(accordion)
194
207
  await waitForWithDelay(() => {
195
208
  const accordionContent = accordion.closest('.accordion-item, .accordion-section, [class*="accordion"]')
@@ -0,0 +1,414 @@
1
+ import { describe, it, expect, afterEach } from 'vitest'
2
+ import { prepareClonedElements } from '../prepareScreenshot'
3
+
4
+ describe('prepareClonedElements', () => {
5
+ // Helper to create DOM structure and append to document
6
+ function createDOM(htmlString: string): HTMLElement {
7
+ const container = document.createElement('div')
8
+ container.innerHTML = htmlString
9
+ document.body.appendChild(container)
10
+ return container
11
+ }
12
+
13
+ // Clean up after each test
14
+ afterEach(() => {
15
+ document.body.innerHTML = ''
16
+ })
17
+
18
+ describe('includeContextInDownload = false', () => {
19
+ it('should return viz-only clone when context not requested', () => {
20
+ const container = createDOM(`
21
+ <div class="dfe-section">
22
+ <h2>Title</h2>
23
+ <p>Text</p>
24
+ <div class="cdc-open-viz-module" data-download-id="viz1">Viz Content</div>
25
+ </div>
26
+ `)
27
+ const viz = container.querySelector('[data-download-id="viz1"]') as HTMLElement
28
+
29
+ const result = prepareClonedElements(viz, false, 'viz1')
30
+
31
+ // When context not requested, clonedTree should be same as clonedViz (just the viz)
32
+ expect(result.clonedTree).toBe(result.clonedViz)
33
+ expect(result.clonedTree.querySelector('h2')).toBeNull()
34
+ expect(result.clonedTree.querySelector('p')).toBeNull()
35
+ })
36
+
37
+ it('should return viz-only even with perfect context available when not requested', () => {
38
+ const container = createDOM(`
39
+ <div class="dfe-section">
40
+ <h2>Perfect Title</h2>
41
+ <p>Perfect description</p>
42
+ <div class="cdc-open-viz-module" data-download-id="viz1">Chart</div>
43
+ </div>
44
+ `)
45
+ const viz = container.querySelector('[data-download-id="viz1"]') as HTMLElement
46
+
47
+ const result = prepareClonedElements(viz, false, 'viz1')
48
+
49
+ // Even though context is available, should not include it
50
+ expect(result.clonedTree).toBe(result.clonedViz)
51
+ expect(result.clonedTree.querySelector('h2')).toBeNull()
52
+ expect(result.clonedTree.querySelector('p')).toBeNull()
53
+ })
54
+ })
55
+
56
+ describe('Basic heading + viz patterns', () => {
57
+ it('should include H2 and paragraph when before viz', () => {
58
+ const container = createDOM(`
59
+ <div class="dfe-section">
60
+ <h2>Emergency Department Visits</h2>
61
+ <p>Weekly percent of total visits.</p>
62
+ <div class="cdc-open-viz-module" data-download-id="viz1">Chart</div>
63
+ </div>
64
+ `)
65
+ const viz = container.querySelector('[data-download-id="viz1"]') as HTMLElement
66
+
67
+ const result = prepareClonedElements(viz, true, 'viz1')
68
+ const children = Array.from(result.clonedTree.children)
69
+
70
+ expect(children.length).toBe(3)
71
+ expect(children[0].tagName).toBe('H2')
72
+ expect(children[1].tagName).toBe('P')
73
+ expect(result.clonedTree.contains(result.clonedViz)).toBe(true)
74
+ })
75
+
76
+ it('should include H3 and paragraph when before viz', () => {
77
+ const container = createDOM(`
78
+ <div class="dfe-section">
79
+ <h3>Emergency Department Visits by Age</h3>
80
+ <p>Weekly percent of total visits.</p>
81
+ <div class="cdc-open-viz-module" data-download-id="viz1">Chart</div>
82
+ </div>
83
+ `)
84
+ const viz = container.querySelector('[data-download-id="viz1"]') as HTMLElement
85
+
86
+ const result = prepareClonedElements(viz, true, 'viz1')
87
+ const children = Array.from(result.clonedTree.children)
88
+
89
+ expect(children.length).toBe(3)
90
+ expect(children[0].tagName).toBe('H3')
91
+ expect(children[1].tagName).toBe('P')
92
+ expect(result.clonedTree.contains(result.clonedViz)).toBe(true)
93
+ })
94
+
95
+ it('should prefer H3 over H2 when both present', () => {
96
+ const container = createDOM(`
97
+ <div class="dfe-section">
98
+ <h2>By Age</h2>
99
+ <h3>Emergency Department Visits by Age</h3>
100
+ <p>Weekly percent of total visits.</p>
101
+ <div class="cdc-open-viz-module" data-download-id="viz1">Chart</div>
102
+ </div>
103
+ `)
104
+ const viz = container.querySelector('[data-download-id="viz1"]') as HTMLElement
105
+
106
+ const result = prepareClonedElements(viz, true, 'viz1')
107
+
108
+ // Should include H3 (nearest to viz) but NOT H2
109
+ expect(result.clonedTree.querySelector('h2')).toBeNull()
110
+ expect(result.clonedTree.querySelector('h3')).not.toBeNull()
111
+ expect(result.clonedTree.querySelector('h3')?.textContent).toBe('Emergency Department Visits by Age')
112
+ })
113
+
114
+ it('should include all paragraphs between heading and viz', () => {
115
+ const container = createDOM(`
116
+ <div class="dfe-section">
117
+ <h3>Emergency Department Visits</h3>
118
+ <p>First paragraph.</p>
119
+ <p>Second paragraph.</p>
120
+ <p>Third paragraph.</p>
121
+ <div class="cdc-open-viz-module" data-download-id="viz1">Chart</div>
122
+ </div>
123
+ `)
124
+ const viz = container.querySelector('[data-download-id="viz1"]') as HTMLElement
125
+
126
+ const result = prepareClonedElements(viz, true, 'viz1')
127
+ const paragraphs = result.clonedTree.querySelectorAll('p')
128
+
129
+ expect(paragraphs.length).toBe(3)
130
+ expect(paragraphs[0].textContent).toBe('First paragraph.')
131
+ expect(paragraphs[1].textContent).toBe('Second paragraph.')
132
+ expect(paragraphs[2].textContent).toBe('Third paragraph.')
133
+ })
134
+ })
135
+
136
+ describe('Multiple visualizations', () => {
137
+ it('should return viz-only when viz2 follows viz1 with no heading between', () => {
138
+ const container = createDOM(`
139
+ <div class="dfe-section">
140
+ <h2>Title</h2>
141
+ <div class="cdc-open-viz-module" data-download-id="viz1">Chart 1</div>
142
+ <div class="cdc-open-viz-module" data-download-id="viz2">Chart 2</div>
143
+ </div>
144
+ `)
145
+ const viz2 = container.querySelector('[data-download-id="viz2"]') as HTMLElement
146
+
147
+ const result = prepareClonedElements(viz2, true, 'viz2')
148
+
149
+ // findNearestHeadingIndex stops at viz1 (returns -1), so we get viz-only
150
+ expect(result.clonedTree).toBe(result.clonedViz)
151
+ })
152
+
153
+ it('should not include heading from previous viz when taking screenshot of second viz', () => {
154
+ const container = createDOM(`
155
+ <div class="dfe-section">
156
+ <h2>First Chart</h2>
157
+ <div class="cdc-open-viz-module" data-download-id="viz1">Chart 1</div>
158
+ <h2>Second Chart</h2>
159
+ <div class="cdc-open-viz-module" data-download-id="viz2">Chart 2</div>
160
+ </div>
161
+ `)
162
+ const viz2 = container.querySelector('[data-download-id="viz2"]') as HTMLElement
163
+
164
+ const result = prepareClonedElements(viz2, true, 'viz2')
165
+
166
+ // Should include "Second Chart" but NOT "First Chart"
167
+ const headings = result.clonedTree.querySelectorAll('h2')
168
+ expect(headings.length).toBe(1)
169
+ expect(headings[0].textContent).toBe('Second Chart')
170
+ })
171
+
172
+ it('should ignore heading inside another viz', () => {
173
+ const container = createDOM(`
174
+ <div class="dfe-section">
175
+ <div class="cdc-open-viz-module" data-download-id="viz1">
176
+ <h2>Title Inside Viz1</h2>
177
+ <div>Chart 1</div>
178
+ </div>
179
+ <div class="cdc-open-viz-module" data-download-id="viz2">Chart 2</div>
180
+ </div>
181
+ `)
182
+ const viz2 = container.querySelector('[data-download-id="viz2"]') as HTMLElement
183
+
184
+ const result = prepareClonedElements(viz2, true, 'viz2')
185
+
186
+ // findNearestHeadingIndex stops at viz1 (returns -1), so we get viz-only
187
+ expect(result.clonedTree).toBe(result.clonedViz)
188
+ })
189
+
190
+ it('should return viz-only when viz3 follows viz1 and viz2', () => {
191
+ const container = createDOM(`
192
+ <div class="dfe-section">
193
+ <h2>Title</h2>
194
+ <div class="cdc-open-viz-module" data-download-id="viz1">Chart 1</div>
195
+ <div class="cdc-open-viz-module" data-download-id="viz2">Chart 2</div>
196
+ <div class="cdc-open-viz-module" data-download-id="viz3">Chart 3</div>
197
+ </div>
198
+ `)
199
+ const viz3 = container.querySelector('[data-download-id="viz3"]') as HTMLElement
200
+
201
+ const result = prepareClonedElements(viz3, true, 'viz3')
202
+
203
+ // Should stop at viz2, return viz-only
204
+ expect(result.clonedTree).toBe(result.clonedViz)
205
+ })
206
+
207
+ it('should prefer H3 over H2 between two vizs', () => {
208
+ const container = createDOM(`
209
+ <div class="dfe-section">
210
+ <div class="cdc-open-viz-module" data-download-id="viz1">Chart 1</div>
211
+ <h2>Section</h2>
212
+ <h3>Subsection</h3>
213
+ <p>Description</p>
214
+ <div class="cdc-open-viz-module" data-download-id="viz2">Chart 2</div>
215
+ </div>
216
+ `)
217
+ const viz2 = container.querySelector('[data-download-id="viz2"]') as HTMLElement
218
+
219
+ const result = prepareClonedElements(viz2, true, 'viz2')
220
+
221
+ // Should include H3 (nearest) but not H2
222
+ expect(result.clonedTree.querySelector('h2')).toBeNull()
223
+ expect(result.clonedTree.querySelector('h3')?.textContent).toBe('Subsection')
224
+ })
225
+ })
226
+
227
+ describe('Section boundaries', () => {
228
+ it('should stop at dfe-section boundary', () => {
229
+ const container = createDOM(`
230
+ <div class="cdc-dfe-body__center">
231
+ <div class="dfe-section">
232
+ <h2>Previous Section</h2>
233
+ <p>Previous content</p>
234
+ <div class="cdc-open-viz-module" data-download-id="viz0">Other viz</div>
235
+ </div>
236
+ <div class="dfe-section">
237
+ <div class="cdc-open-viz-module" data-download-id="viz1">Chart</div>
238
+ </div>
239
+ </div>
240
+ `)
241
+ const viz = container.querySelector('[data-download-id="viz1"]') as HTMLElement
242
+
243
+ const result = prepareClonedElements(viz, true, 'viz1')
244
+
245
+ // Should return viz-only (stopped at section boundary, no heading in same section)
246
+ expect(result.clonedTree).toBe(result.clonedViz)
247
+ expect(result.clonedTree.querySelector('h2')).toBeNull()
248
+ expect(result.clonedTree.textContent).not.toContain('Previous Section')
249
+ })
250
+
251
+ it('should work without dfe-section wrapper', () => {
252
+ const container = createDOM(`
253
+ <div class="some-container">
254
+ <h2>Title</h2>
255
+ <p>Description</p>
256
+ <div class="cdc-open-viz-module" data-download-id="viz1">Chart</div>
257
+ </div>
258
+ `)
259
+ const viz = container.querySelector('[data-download-id="viz1"]') as HTMLElement
260
+
261
+ const result = prepareClonedElements(viz, true, 'viz1')
262
+ const children = Array.from(result.clonedTree.children)
263
+
264
+ // Should include H2 and P even without dfe-section
265
+ expect(children.length).toBe(3)
266
+ expect(result.clonedTree.querySelector('h2')?.textContent).toBe('Title')
267
+ expect(result.clonedTree.querySelector('p')?.textContent).toBe('Description')
268
+ })
269
+
270
+ it('should stop at <section> tag boundary', () => {
271
+ const container = createDOM(`
272
+ <div class="outer-container">
273
+ <h2>Title Above Section</h2>
274
+ <section>
275
+ <div class="cdc-open-viz-module" data-download-id="viz1">Chart</div>
276
+ </section>
277
+ </div>
278
+ `)
279
+ const viz = container.querySelector('[data-download-id="viz1"]') as HTMLElement
280
+
281
+ const result = prepareClonedElements(viz, true, 'viz1')
282
+
283
+ // findParentWithContext stops at <section> boundary (returns null), so viz-only
284
+ expect(result.clonedTree).toBe(result.clonedViz)
285
+ })
286
+
287
+ it('should stop at innermost section boundary when nested', () => {
288
+ const container = createDOM(`
289
+ <div class="dfe-section">
290
+ <h2>Outer Title</h2>
291
+ <section>
292
+ <div class="cdc-open-viz-module" data-download-id="viz1">Chart</div>
293
+ </section>
294
+ </div>
295
+ `)
296
+ const viz = container.querySelector('[data-download-id="viz1"]') as HTMLElement
297
+
298
+ const result = prepareClonedElements(viz, true, 'viz1')
299
+
300
+ // Should stop at <section>, not climb to dfe-section
301
+ expect(result.clonedTree).toBe(result.clonedViz)
302
+ expect(result.clonedTree.textContent).not.toContain('Outer Title')
303
+ })
304
+
305
+ it('should include context when section tag is the parent with heading', () => {
306
+ const container = createDOM(`
307
+ <section>
308
+ <h2>Section Title</h2>
309
+ <p>Description</p>
310
+ <div class="cdc-open-viz-module" data-download-id="viz1">Chart</div>
311
+ </section>
312
+ `)
313
+ const viz = container.querySelector('[data-download-id="viz1"]') as HTMLElement
314
+
315
+ const result = prepareClonedElements(viz, true, 'viz1')
316
+
317
+ // Section with heading inside should work
318
+ expect(result.clonedTree.querySelector('h2')?.textContent).toBe('Section Title')
319
+ expect(result.clonedTree.querySelector('p')?.textContent).toBe('Description')
320
+ })
321
+ })
322
+
323
+ describe('Edge cases', () => {
324
+ it('should return viz-only when only paragraph (no heading) before viz', () => {
325
+ const container = createDOM(`
326
+ <div class="dfe-section">
327
+ <p>Some paragraph without heading</p>
328
+ <div class="cdc-open-viz-module" data-download-id="viz1">Chart</div>
329
+ </div>
330
+ `)
331
+ const viz = container.querySelector('[data-download-id="viz1"]') as HTMLElement
332
+
333
+ const result = prepareClonedElements(viz, true, 'viz1')
334
+
335
+ // Should return viz-only (no heading found)
336
+ expect(result.clonedTree).toBe(result.clonedViz)
337
+ expect(result.clonedTree.querySelector('p')).toBeNull()
338
+ })
339
+
340
+ it('should not include content after viz', () => {
341
+ const container = createDOM(`
342
+ <div class="dfe-section">
343
+ <h2>Title</h2>
344
+ <div class="cdc-open-viz-module" data-download-id="viz1">Chart</div>
345
+ <p>Content after viz should not be included</p>
346
+ </div>
347
+ `)
348
+ const viz = container.querySelector('[data-download-id="viz1"]') as HTMLElement
349
+
350
+ const result = prepareClonedElements(viz, true, 'viz1')
351
+
352
+ // Should include H2 and viz, but NOT the paragraph after
353
+ expect(result.clonedTree.querySelector('h2')).not.toBeNull()
354
+ expect(result.clonedTree.textContent).not.toContain('Content after viz should not be included')
355
+ })
356
+
357
+ it('should handle direct H2 element as child', () => {
358
+ const container = createDOM(`
359
+ <div class="dfe-section">
360
+ <h2>Direct H2 Child</h2>
361
+ <div class="cdc-open-viz-module" data-download-id="viz1">Chart</div>
362
+ </div>
363
+ `)
364
+ const viz = container.querySelector('[data-download-id="viz1"]') as HTMLElement
365
+
366
+ const result = prepareClonedElements(viz, true, 'viz1')
367
+
368
+ // Should find H2 even though it's a direct child (not nested in div)
369
+ expect(result.clonedTree.querySelector('h2')?.textContent).toBe('Direct H2 Child')
370
+ })
371
+
372
+ it('should handle H2 nested in div', () => {
373
+ const container = createDOM(`
374
+ <div class="dfe-section">
375
+ <div class="heading-wrapper">
376
+ <h2>Nested H2</h2>
377
+ </div>
378
+ <div class="cdc-open-viz-module" data-download-id="viz1">Chart</div>
379
+ </div>
380
+ `)
381
+ const viz = container.querySelector('[data-download-id="viz1"]') as HTMLElement
382
+
383
+ const result = prepareClonedElements(viz, true, 'viz1')
384
+
385
+ // Should find H2 even when nested
386
+ expect(result.clonedTree.querySelector('h2')?.textContent).toBe('Nested H2')
387
+ expect(result.clonedTree.querySelector('.heading-wrapper')).not.toBeNull()
388
+ })
389
+
390
+ it('should handle nested visualization wrapper', () => {
391
+ const container = createDOM(`
392
+ <div class="dfe-section">
393
+ <h2>Title</h2>
394
+ <p>Description</p>
395
+ <div class="outer-wrapper">
396
+ <div class="inner-wrapper">
397
+ <div class="cdc-open-viz-module" data-download-id="viz1">Chart</div>
398
+ </div>
399
+ </div>
400
+ </div>
401
+ `)
402
+ const viz = container.querySelector('[data-download-id="viz1"]') as HTMLElement
403
+
404
+ const result = prepareClonedElements(viz, true, 'viz1')
405
+
406
+ // Should include H2, P, and nested wrappers with viz
407
+ expect(result.clonedTree.querySelector('h2')).not.toBeNull()
408
+ expect(result.clonedTree.querySelector('p')).not.toBeNull()
409
+ expect(result.clonedTree.querySelector('.outer-wrapper')).not.toBeNull()
410
+ expect(result.clonedTree.querySelector('.inner-wrapper')).not.toBeNull()
411
+ expect(result.clonedViz.textContent).toBe('Chart')
412
+ })
413
+ })
414
+ })