@cdc/markup-include 4.26.3 → 4.26.5

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.
@@ -1,10 +1,11 @@
1
- import { useContext, useRef } from 'react'
1
+ import { useContext, useMemo, useRef } from 'react'
2
2
 
3
3
  // Context
4
4
  import ConfigContext from '../../ConfigContext'
5
5
 
6
6
  // Helpers
7
7
  import { updateFieldFactory } from '@cdc/core/helpers/updateFieldFactory'
8
+ import { useDataColumns } from '@cdc/core/hooks/useDataColumns'
8
9
 
9
10
  // Components
10
11
  import { EditorPanel as BaseEditorPanel } from '@cdc/core/components/EditorPanel/EditorPanel'
@@ -15,7 +16,11 @@ import Tooltip from '@cdc/core/components/ui/Tooltip'
15
16
  import MarkupVariablesEditor from '@cdc/core/components/EditorPanel/components/MarkupVariablesEditor'
16
17
  import FootnotesEditor from '@cdc/core/components/EditorPanel/FootnotesEditor'
17
18
  import StyleTreatmentSection from '@cdc/core/components/EditorPanel/sections/StyleTreatmentSection'
19
+ import { Tp5VisualSection } from '@cdc/core/components/EditorPanel/sections/Tp5VisualSection'
18
20
  import { HeaderThemeSelector } from '@cdc/core/components/HeaderThemeSelector'
21
+ import { DataColorSelector } from '@cdc/core/components/DataColorSelector'
22
+ import { DATA_COLOR_PRESETS } from '@cdc/core/helpers/dataColors'
23
+ import Button from '@cdc/core/components/elements/Button'
19
24
  import { Datasets } from '@cdc/core/types/DataSet'
20
25
 
21
26
  // styles
@@ -26,11 +31,106 @@ type MarkupIncludeEditorPanelProps = {
26
31
  }
27
32
 
28
33
  const EditorPanel: React.FC<MarkupIncludeEditorPanelProps> = ({ datasets }) => {
29
- const { config, data, isDashboard, loading, setParentConfig, updateConfig } = useContext(ConfigContext)
34
+ const { config, data, editorData, isDashboard, loading, setParentConfig, updateConfig } = useContext(ConfigContext)
30
35
  const { contentEditor, theme, visual } = config || {}
31
36
  const { inlineHTML, srcUrl, title, useInlineHTML } = contentEditor || {}
37
+ const isTp5Style = contentEditor?.style === 'tp5'
32
38
  const updateField = updateFieldFactory(config, updateConfig, true)
33
- const styleTreatment = visual?.tp5Treatment ? 'tp5' : 'legacy'
39
+ const styleTreatment = (visual as any)?.tp5Treatment ? 'tp5' : 'legacy'
40
+ const markupEditorData = Array.isArray(editorData) ? editorData : data || []
41
+ const columns = useDataColumns(markupEditorData)
42
+ const dataColorMappings = config.dataColors?.mappings || []
43
+
44
+ const dataColorValues = useMemo(() => {
45
+ const colorColumn = config.dataColors?.column
46
+ if (!colorColumn) return []
47
+
48
+ const uniqueValues = new Set<string>()
49
+ markupEditorData?.forEach(row => {
50
+ const value = row?.[colorColumn]
51
+ if (value !== undefined && value !== null) {
52
+ uniqueValues.add(String(value))
53
+ }
54
+ })
55
+
56
+ return Array.from(uniqueValues).sort()
57
+ }, [markupEditorData, config.dataColors?.column])
58
+
59
+ type DataColorDisplayEntry = { sourceValue: string; fromData: boolean; key: string }
60
+
61
+ const dataColorDisplayList = useMemo<DataColorDisplayEntry[]>(() => {
62
+ const dataSet = new Set(dataColorValues)
63
+ const list: DataColorDisplayEntry[] = dataColorValues.map(v => ({
64
+ sourceValue: v,
65
+ fromData: true,
66
+ key: `data-${v}`
67
+ }))
68
+ dataColorMappings.forEach((m, i) => {
69
+ if (!dataSet.has(m.sourceValue)) {
70
+ list.push({ sourceValue: m.sourceValue, fromData: false, key: `custom-${i}` })
71
+ }
72
+ })
73
+ return list
74
+ }, [dataColorValues, dataColorMappings])
75
+
76
+ const updateDataColorMapping = (sourceValue: string, color: string) => {
77
+ const nextMappings = [...dataColorMappings]
78
+ const existingIndex = nextMappings.findIndex(m => m.sourceValue === sourceValue)
79
+
80
+ if (!color) {
81
+ if (existingIndex > -1) {
82
+ nextMappings.splice(existingIndex, 1)
83
+ }
84
+ } else {
85
+ const nextMapping = { sourceValue, color }
86
+ if (existingIndex > -1) {
87
+ nextMappings[existingIndex] = nextMapping
88
+ } else {
89
+ nextMappings.push(nextMapping)
90
+ }
91
+ }
92
+
93
+ updateConfig({
94
+ ...config,
95
+ dataColors: {
96
+ ...config.dataColors,
97
+ mappings: nextMappings
98
+ }
99
+ })
100
+ }
101
+
102
+ const addCustomDataColorMapping = () => {
103
+ const nextMappings = [...dataColorMappings, { sourceValue: '', color: DATA_COLOR_PRESETS[0] }]
104
+ updateConfig({
105
+ ...config,
106
+ dataColors: {
107
+ ...config.dataColors,
108
+ mappings: nextMappings
109
+ }
110
+ })
111
+ }
112
+
113
+ const updateDataColorMappingValue = (oldValue: string, newValue: string) => {
114
+ const nextMappings = dataColorMappings.map(m => (m.sourceValue === oldValue ? { ...m, sourceValue: newValue } : m))
115
+ updateConfig({
116
+ ...config,
117
+ dataColors: {
118
+ ...config.dataColors,
119
+ mappings: nextMappings
120
+ }
121
+ })
122
+ }
123
+
124
+ const removeDataColorMapping = (sourceValue: string) => {
125
+ const nextMappings = dataColorMappings.filter(m => m.sourceValue !== sourceValue)
126
+ updateConfig({
127
+ ...config,
128
+ dataColors: {
129
+ ...config.dataColors,
130
+ mappings: nextMappings
131
+ }
132
+ })
133
+ }
34
134
 
35
135
  const handleStyleTreatmentChange = (value: string) => {
36
136
  const useTp5Treatment = value === 'tp5'
@@ -74,6 +174,17 @@ const EditorPanel: React.FC<MarkupIncludeEditorPanelProps> = ({ datasets }) => {
74
174
  {() => (
75
175
  <Accordion>
76
176
  <Accordion.Section title='General'>
177
+ <Select
178
+ value={contentEditor?.style || 'default'}
179
+ section='contentEditor'
180
+ fieldName='style'
181
+ label='Style'
182
+ updateField={updateField}
183
+ options={[
184
+ { value: 'default', label: 'Default' },
185
+ { value: 'tp5', label: 'TP5' }
186
+ ]}
187
+ />
77
188
  <TextField
78
189
  value={title || ''}
79
190
  section='contentEditor'
@@ -82,32 +193,34 @@ const EditorPanel: React.FC<MarkupIncludeEditorPanelProps> = ({ datasets }) => {
82
193
  placeholder='Markup Include Title'
83
194
  updateField={updateField}
84
195
  />
85
- <Select
86
- value={contentEditor?.titleStyle || 'small'}
87
- section='contentEditor'
88
- fieldName='titleStyle'
89
- label='Title Style'
90
- updateField={updateField}
91
- options={[
92
- { value: 'small', label: 'Small (h3)' },
93
- { value: 'large', label: 'Large (h2)' },
94
- { value: 'legacy', label: 'Legacy' }
95
- ]}
96
- tooltip={
97
- <Tooltip style={{ textTransform: 'none' }}>
98
- <Tooltip.Target>
99
- <Icon display='question' style={{ marginLeft: '0.5rem' }} />
100
- </Tooltip.Target>
101
- <Tooltip.Content>
102
- <p>Choose the visual style for the title.</p>
103
- <p>
104
- Consider heading order on your page when selecting the title style. For 508 reasons, ensure your
105
- page follows a proper heading order.
106
- </p>
107
- </Tooltip.Content>
108
- </Tooltip>
109
- }
110
- />
196
+ {!isTp5Style && (
197
+ <Select
198
+ value={contentEditor?.titleStyle || 'small'}
199
+ section='contentEditor'
200
+ fieldName='titleStyle'
201
+ label='Title Style'
202
+ updateField={updateField}
203
+ options={[
204
+ { value: 'small', label: 'Small (h3)' },
205
+ { value: 'large', label: 'Large (h2)' },
206
+ { value: 'legacy', label: 'Legacy' }
207
+ ]}
208
+ tooltip={
209
+ <Tooltip style={{ textTransform: 'none' }}>
210
+ <Tooltip.Target>
211
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
212
+ </Tooltip.Target>
213
+ <Tooltip.Content>
214
+ <p>Choose the visual style for the title.</p>
215
+ <p>
216
+ Consider heading order on your page when selecting the title style. For 508 reasons, ensure your
217
+ page follows a proper heading order.
218
+ </p>
219
+ </Tooltip.Content>
220
+ </Tooltip>
221
+ }
222
+ />
223
+ )}
111
224
  <Select
112
225
  value={config.locale}
113
226
  fieldName='locale'
@@ -170,24 +283,105 @@ const EditorPanel: React.FC<MarkupIncludeEditorPanelProps> = ({ datasets }) => {
170
283
  </div>
171
284
  </Accordion.Section>
172
285
  <Accordion.Section title='Visual'>
173
- <HeaderThemeSelector
174
- selectedTheme={config.theme}
175
- onThemeSelect={theme => updateConfig({ ...config, theme })}
176
- />
177
- <StyleTreatmentSection
178
- styleTreatment={styleTreatment}
179
- onStyleTreatmentChange={handleStyleTreatmentChange}
180
- showStyleTreatment={false}
181
- border={config.visual?.border}
182
- borderColorTheme={config.visual?.borderColorTheme}
183
- accent={config.visual?.accent}
184
- background={config.visual?.background}
185
- hideBackgroundColor={config.visual?.hideBackgroundColor}
186
- showBackground
187
- showHideBackgroundColor
188
- updateField={updateField}
189
- />
286
+ {!isTp5Style && (
287
+ <HeaderThemeSelector
288
+ selectedTheme={config.theme}
289
+ onThemeSelect={theme => updateConfig({ ...config, theme })}
290
+ />
291
+ )}
292
+ {isTp5Style ? (
293
+ <Tp5VisualSection config={config} updateField={updateField} />
294
+ ) : (
295
+ <StyleTreatmentSection
296
+ styleTreatment={styleTreatment}
297
+ onStyleTreatmentChange={handleStyleTreatmentChange}
298
+ showStyleTreatment={false}
299
+ border={config.visual?.border}
300
+ borderColorTheme={config.visual?.borderColorTheme}
301
+ accent={config.visual?.accent}
302
+ background={config.visual?.background}
303
+ hideBackgroundColor={config.visual?.hideBackgroundColor}
304
+ showBackground
305
+ showHideBackgroundColor
306
+ updateField={updateField}
307
+ />
308
+ )}
190
309
  </Accordion.Section>
310
+ {isTp5Style && (
311
+ <Accordion.Section title='Data-Driven Colors'>
312
+ <Select
313
+ value={config.dataColors?.column || ''}
314
+ section='dataColors'
315
+ fieldName='column'
316
+ label='Color Column'
317
+ updateField={updateField}
318
+ initial='Select'
319
+ options={columns}
320
+ tooltip={
321
+ <Tooltip style={{ textTransform: 'none' }}>
322
+ <Tooltip.Target>
323
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
324
+ </Tooltip.Target>
325
+ <Tooltip.Content>
326
+ <p>
327
+ Choose a column whose values determine the background color of this visualization. Map each
328
+ value to a color below. Text color adjusts automatically for contrast.
329
+ </p>
330
+ </Tooltip.Content>
331
+ </Tooltip>
332
+ }
333
+ />
334
+ {config.dataColors?.column && dataColorDisplayList.length > 0 && (
335
+ <div className='mt-2'>
336
+ {dataColorDisplayList.map(({ sourceValue, fromData, key }) => {
337
+ const selectedColor = dataColorMappings.find(m => m.sourceValue === sourceValue)?.color || ''
338
+
339
+ return (
340
+ <div className='cove-accordion__panel-row align-center mb-2' key={key}>
341
+ <div className='cove-accordion__panel-col' style={{ flex: '1 1 0', minWidth: 0 }}>
342
+ {fromData ? (
343
+ sourceValue
344
+ ) : (
345
+ <input
346
+ type='text'
347
+ value={sourceValue}
348
+ placeholder='Enter value'
349
+ style={{ width: '100%' }}
350
+ onChange={e => updateDataColorMappingValue(sourceValue, e.target.value)}
351
+ />
352
+ )}
353
+ </div>
354
+ <div className='cove-accordion__panel-col' style={{ flex: '0 0 4.5rem' }}>
355
+ <DataColorSelector
356
+ value={selectedColor}
357
+ onChange={color => updateDataColorMapping(sourceValue, color)}
358
+ />
359
+ </div>
360
+ <div className='cove-accordion__panel-col' style={{ flex: '0 0 1.5rem' }}>
361
+ {!fromData && (
362
+ <button
363
+ type='button'
364
+ className='btn btn-danger'
365
+ style={{ padding: '0.15rem 0.45rem', lineHeight: 1 }}
366
+ title='Remove mapping'
367
+ onClick={() => removeDataColorMapping(sourceValue)}
368
+ >
369
+
370
+ </button>
371
+ )}
372
+ </div>
373
+ </div>
374
+ )
375
+ })}
376
+ </div>
377
+ )}
378
+ {config.dataColors?.column && (
379
+ <Button type='button' onClick={addCustomDataColorMapping} className='btn btn-primary full-width mt-3'>
380
+ Add Color Mapping
381
+ </Button>
382
+ )}
383
+ </Accordion.Section>
384
+ )}
191
385
  {isDashboard && (
192
386
  <Accordion.Section title='Footnotes'>
193
387
  <FootnotesEditor config={config} updateField={updateField} datasets={datasets} />
@@ -196,7 +390,8 @@ const EditorPanel: React.FC<MarkupIncludeEditorPanelProps> = ({ datasets }) => {
196
390
  <Accordion.Section title='Markup Variables'>
197
391
  <MarkupVariablesEditor
198
392
  markupVariables={config.markupVariables || []}
199
- data={data || []}
393
+ data={Array.isArray(data) ? data : []}
394
+ editorData={markupEditorData}
200
395
  datasets={datasets}
201
396
  config={config}
202
397
  onChange={handleMarkupVariablesChange}
@@ -2,6 +2,7 @@ export default {
2
2
  contentEditor: {
3
3
  inlineHTML: '<strong>Inline HTML</strong>',
4
4
  showHeader: true,
5
+ style: 'default',
5
6
  srcUrl: '#example',
6
7
  title: '',
7
8
  titleStyle: 'small',
@@ -22,7 +23,12 @@ export default {
22
23
  accent: false,
23
24
  background: false,
24
25
  hideBackgroundColor: false,
25
- borderColorTheme: false
26
+ borderColorTheme: false,
27
+ whiteBackground: false
28
+ },
29
+ dataColors: {
30
+ column: '',
31
+ mappings: []
26
32
  },
27
33
  markupVariables: [],
28
34
  enableMarkupVariables: false
package/src/index.tsx CHANGED
@@ -9,14 +9,17 @@ import CdcMarkupInclude from './CdcMarkupInclude'
9
9
 
10
10
  let isEditor = window.location.href.includes('editor=true')
11
11
 
12
- let domContainer = document.getElementsByClassName('react-container')[0]
12
+ let domContainer = document.getElementsByClassName('react-container')[0] as HTMLElement & { coveConfig?: any }
13
+ let configUrl = domContainer.dataset.configUrl
14
+ let injectedConfig = domContainer.coveConfig
13
15
 
14
16
  ReactDOM.createRoot(domContainer).render(
15
17
  <React.StrictMode>
16
18
  <GlobalContextProvider>
17
19
  <CdcMarkupInclude
18
- configUrl={domContainer.attributes['data-config'].value}
19
- interactionLabel={domContainer.attributes['data-config'].value}
20
+ config={injectedConfig}
21
+ configUrl={injectedConfig ? undefined : configUrl}
22
+ interactionLabel={configUrl}
20
23
  isEditor={isEditor}
21
24
  />
22
25
  </GlobalContextProvider>
@@ -1,10 +1,72 @@
1
1
  @import '@cdc/core/styles/layout/wrapper-padding';
2
+ @import '@cdc/core/styles/layout/callout';
2
3
 
3
4
  .cove-visualization.type-markup-include {
4
5
  .cove-visualization__body {
5
6
  @include cove-visualization-body-padding;
6
7
  }
7
8
 
9
+ .markup-include-component--tp5 {
10
+ padding: 0 !important;
11
+ border: none !important;
12
+ background: none !important;
13
+ container-type: inline-size;
14
+
15
+ .markup-include-tp5 {
16
+ gap: 0.7rem;
17
+ }
18
+
19
+ .cdc-callout__heading {
20
+ width: 100%;
21
+ font-size: 1.1rem;
22
+ }
23
+
24
+ .cdc-callout__body {
25
+ flex-wrap: nowrap;
26
+ flex: auto;
27
+ }
28
+
29
+ @container (max-width: 576px) {
30
+ .cdc-callout__body {
31
+ flex-wrap: wrap;
32
+ }
33
+ }
34
+
35
+ .cdc-callout__content {
36
+ font-size: 1rem;
37
+ }
38
+
39
+ .cdc-callout--data-color {
40
+ .cdc-callout__heading,
41
+ .cdc-callout__content {
42
+ color: inherit;
43
+ }
44
+ }
45
+
46
+ &.white-background-style {
47
+ .markup-include-tp5 {
48
+ border: 1px solid #009ec1 !important;
49
+ background: transparent;
50
+ box-shadow: 0 2px 4px rgb(159 159 159 / 10%);
51
+ }
52
+
53
+ .cdc-callout--data-color {
54
+ border: none !important;
55
+ }
56
+ }
57
+
58
+ &.white-background-style.display-border {
59
+ .markup-include-tp5 {
60
+ box-shadow: 0 2px 4px rgb(159 159 159 / 10%);
61
+ }
62
+ }
63
+
64
+ .cove-visualization__body-wrap,
65
+ .cove-visualization__content-section {
66
+ padding: 0;
67
+ }
68
+ }
69
+
8
70
  .cove-visualization__body-wrap {
9
71
  @include cove-visualization-body-wrap-inline-padding;
10
72
 
@@ -22,7 +84,7 @@
22
84
  // keep markup include images inlines on dashboards
23
85
  // e.g. requested to help here: https://www.cdc.gov/fluview/surveillance/2026-week-10.html
24
86
  // needed for backwards compatibility with existing dashboards, but also to prevent any future issues with images in markup include visualizations being forced to display as blocks
25
- span>img {
87
+ span > img {
26
88
  display: inline;
27
89
  }
28
90
  }
@@ -54,4 +116,4 @@
54
116
  .cove-editor .cove-editor__content {
55
117
  padding-left: 350px;
56
118
  display: flex;
57
- }
119
+ }
@@ -1,6 +1,30 @@
1
1
  import path from 'node:path'
2
+ import fs from 'node:fs'
3
+ import vm from 'node:vm'
4
+ import React from 'react'
5
+ import { render, screen, waitFor } from '@testing-library/react'
2
6
  import { testStandaloneBuild } from '@cdc/core/helpers/tests/testStandaloneBuild.ts'
3
- import { describe, it, expect } from 'vitest'
7
+ import { describe, it, expect, vi } from 'vitest'
8
+ import CdcMarkupInclude from '../CdcMarkupInclude'
9
+
10
+ vi.mock('@cdc/core/components/EditorPanel/components/MarkupVariablesEditor', () => ({
11
+ default: ({ data, editorData }) => (
12
+ <div>
13
+ <div data-testid='markup-variables-editor-data'>{JSON.stringify(data)}</div>
14
+ <div data-testid='markup-variables-editor-editor-data'>{JSON.stringify(editorData)}</div>
15
+ </div>
16
+ )
17
+ }))
18
+
19
+ const extractMarkedExampleConfig = (content, label) => {
20
+ const match = content.match(
21
+ /<!-- README_EXAMPLE_CONFIG_START -->\s*```jsx\s*([\s\S]*?)\s*```\s*<!-- README_EXAMPLE_CONFIG_END -->/
22
+ )
23
+ expect(match, `${label} should contain a marked README example block`).toBeTruthy()
24
+ const configMatch = match[1].match(/const config = (\{[\s\S]*?\})\n\nfunction App\(\)/)
25
+ expect(configMatch, `${label} should define const config before function App()`).toBeTruthy()
26
+ return vm.runInNewContext(`(${configMatch[1]})`)
27
+ }
4
28
 
5
29
  describe('Markup Include', () => {
6
30
  it('Can be built in isolation', async () => {
@@ -8,4 +32,151 @@ describe('Markup Include', () => {
8
32
  const result = await testStandaloneBuild(pkgDir)
9
33
  expect(result).toBe(true)
10
34
  }, 300000)
35
+
36
+ it('uses dashboard rawData for markup variable editor choices while preserving render data when editing', async () => {
37
+ const filteredData = [{ category: 'Filtered only' }]
38
+ const fullData = [{ category: 'Value A' }, { category: 'Value B' }]
39
+
40
+ render(
41
+ <CdcMarkupInclude
42
+ config={{
43
+ type: 'markup-include',
44
+ theme: 'theme-blue',
45
+ dataKey: 'test-dataset',
46
+ data: filteredData,
47
+ markupVariables: [],
48
+ contentEditor: {
49
+ title: '',
50
+ inlineHTML: '<p>Example</p>',
51
+ useInlineHTML: true,
52
+ srcUrl: ''
53
+ },
54
+ visual: {
55
+ border: false,
56
+ accent: false,
57
+ background: false,
58
+ hideBackgroundColor: false,
59
+ borderColorTheme: false
60
+ }
61
+ }}
62
+ datasets={{
63
+ 'test-dataset': {
64
+ data: [{ category: 'Dataset fallback' }]
65
+ }
66
+ }}
67
+ rawData={fullData}
68
+ isDashboard={true}
69
+ isEditor={true}
70
+ />
71
+ )
72
+
73
+ expect(JSON.parse((await screen.findByTestId('markup-variables-editor-data')).textContent)).toEqual(filteredData)
74
+ expect(JSON.parse((await screen.findByTestId('markup-variables-editor-editor-data')).textContent)).toEqual(fullData)
75
+ })
76
+
77
+ it('does not render the visualization filters section when every markup include filter is hidden', async () => {
78
+ const { container } = render(
79
+ <CdcMarkupInclude
80
+ config={{
81
+ type: 'markup-include',
82
+ theme: 'theme-blue',
83
+ data: [{ state: 'Alabama' }],
84
+ filters: [
85
+ {
86
+ columnName: 'state',
87
+ values: ['Alabama'],
88
+ showDropdown: false,
89
+ id: 1,
90
+ parents: [],
91
+ active: 'Alabama',
92
+ filterStyle: 'dropdown',
93
+ label: 'State',
94
+ order: 'asc',
95
+ type: 'url'
96
+ }
97
+ ],
98
+ markupVariables: [],
99
+ contentEditor: {
100
+ title: '',
101
+ inlineHTML: '<p>Example</p>',
102
+ useInlineHTML: true,
103
+ srcUrl: ''
104
+ },
105
+ visual: {
106
+ border: false,
107
+ accent: false,
108
+ background: false,
109
+ hideBackgroundColor: false,
110
+ borderColorTheme: false
111
+ }
112
+ }}
113
+ datasets={{}}
114
+ isDashboard={true}
115
+ />
116
+ )
117
+
118
+ await waitFor(() => expect(container.querySelector('.markup-include-content-container')).toBeInTheDocument())
119
+
120
+ expect(container.querySelector('.cove-visualization__filters-section')).not.toBeInTheDocument()
121
+ })
122
+
123
+ it('keeps included bootstrap column markup inside the shared content section', async () => {
124
+ const { container } = render(
125
+ <CdcMarkupInclude
126
+ config={{
127
+ type: 'markup-include',
128
+ theme: 'theme-blue',
129
+ markupVariables: [],
130
+ contentEditor: {
131
+ title: 'Grid mock',
132
+ inlineHTML: `
133
+ <div class="row">
134
+ <div class="col-8"><p>Primary content</p></div>
135
+ <div class="col-3"><p>Secondary content</p></div>
136
+ </div>
137
+ `,
138
+ useInlineHTML: true,
139
+ srcUrl: ''
140
+ },
141
+ visual: {
142
+ border: false,
143
+ accent: false,
144
+ background: false,
145
+ hideBackgroundColor: false,
146
+ borderColorTheme: false
147
+ }
148
+ }}
149
+ datasets={{}}
150
+ isDashboard={true}
151
+ />
152
+ )
153
+
154
+ await waitFor(() => expect(container.querySelector('.markup-include-content-container')).toBeInTheDocument())
155
+
156
+ const shell = container.querySelector('.cove-visualization.type-markup-include')
157
+ const contentSection = container.querySelector('.cove-visualization__content-section')
158
+ const primaryColumn = container.querySelector('.col-8')
159
+ const secondaryColumn = container.querySelector('.col-3')
160
+
161
+ expect(shell).toBeInTheDocument()
162
+ expect(contentSection).toBeInTheDocument()
163
+ expect(primaryColumn).toBeInTheDocument()
164
+ expect(secondaryColumn).toBeInTheDocument()
165
+ expect(primaryColumn?.closest('.cove-visualization__content-section')).toBe(contentSection)
166
+ expect(secondaryColumn?.closest('.cove-visualization__content-section')).toBe(contentSection)
167
+ expect(primaryColumn?.parentElement).not.toBe(shell)
168
+ expect(secondaryColumn?.parentElement).not.toBe(shell)
169
+ })
170
+
171
+ it('keeps the minimal example in sync with the README docs', () => {
172
+ const pkgRoot = path.join(__dirname, '..', '..')
173
+ const minimalExamplePath = path.join(pkgRoot, 'examples', 'minimal-example.json')
174
+ const readmePath = path.join(pkgRoot, 'README.md')
175
+
176
+ const minimalExample = JSON.parse(fs.readFileSync(minimalExamplePath, 'utf8'))
177
+ const readmeBlock = extractMarkedExampleConfig(fs.readFileSync(readmePath, 'utf8'), 'README.md')
178
+
179
+ expect(readmeBlock).toEqual(minimalExample)
180
+ expect(minimalExample.version).toBeTruthy()
181
+ })
11
182
  })