@cdc/markup-include 4.25.8 → 4.25.11

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.
@@ -0,0 +1,465 @@
1
+ /**
2
+ * MARKUP INCLUDE EDITOR TESTS
3
+ *
4
+ * Comprehensive test suite for the Markup Include component editor functionality.
5
+ * Tests all controls and verifies that editor interactions produce expected visual changes.
6
+ *
7
+ * RUNNING TESTS:
8
+ * ==============
9
+ * # Run all markup-include editor tests:
10
+ * npx vitest run --project=storybook packages/markup-include/src/_stories/*.Editor.stories.tsx
11
+ *
12
+ * # Run specific test story:
13
+ * npx vitest run --project=storybook packages/markup-include/src/_stories/MarkupInclude.Editor.stories.tsx -t "GeneralSectionTests"
14
+ *
15
+ * # Run tests in watch mode:
16
+ * npx vitest --project=storybook packages/markup-include/src/_stories/*.Editor.stories.tsx
17
+ *
18
+ * TEST COVERAGE:
19
+ * ==============
20
+ * ✅ General Section:
21
+ * - Title input and header display
22
+ *
23
+ * ✅ Content Editor Section:
24
+ * - Use Inline HTML toggle (switches between textarea and URL input)
25
+ * - HTML content editing and preview
26
+ * - Source URL input
27
+ * - Variable management (create, edit, delete)
28
+ * - Variable configuration (name, column, conditions, formatting)
29
+ * - Advanced options (Hide Section on Null, No Data Message)
30
+ *
31
+ * ✅ Visual Section:
32
+ * - Theme palette selection
33
+ * - Border display toggle
34
+ * - Border color theme toggle
35
+ * - Accent style toggle
36
+ * - Theme background color toggle
37
+ * - Hide background color toggle
38
+ *
39
+ * TESTING APPROACH:
40
+ * =================
41
+ * - Follows CDC testing best practices (visual-first testing)
42
+ * - Uses performAndAssert for state changes
43
+ * - Tests visual output changes, not just control state
44
+ * - Leverages shared testing helpers from @cdc/core/helpers/testing
45
+ * - Each test story focuses on one accordion section
46
+ * - Includes comprehensive variable system testing
47
+ */
48
+
49
+ import type { Meta, StoryObj } from '@storybook/react-vite'
50
+ import { within, userEvent, expect } from 'storybook/test'
51
+ import CdcMarkupInclude from '../CdcMarkupInclude'
52
+ import {
53
+ performAndAssert,
54
+ waitForPresence,
55
+ waitForAbsence,
56
+ waitForEditor,
57
+ openAccordion,
58
+ getVisualState,
59
+ testBooleanControl
60
+ } from '@cdc/core/helpers/testing'
61
+
62
+ const meta: Meta<typeof CdcMarkupInclude> = {
63
+ title: 'Components/Templates/Markup Include/Editor Tests',
64
+ component: CdcMarkupInclude,
65
+ parameters: {
66
+ layout: 'fullscreen'
67
+ }
68
+ }
69
+
70
+ export default meta
71
+ type Story = StoryObj<typeof CdcMarkupInclude>
72
+
73
+ // Test configurations - using both inline config and external fixtures
74
+ const testConfig = {
75
+ contentEditor: {
76
+ inlineHTML: '<h2>Test Markup Include</h2><p>{{test_variable}}</p>',
77
+ showHeader: true,
78
+ srcUrl: '',
79
+ title: 'Test Markup Include Title',
80
+ useInlineHTML: true,
81
+ showNoDataMessage: false,
82
+ noDataMessageText: 'No data available'
83
+ },
84
+ data: [
85
+ { state: 'Alabama', value: 100, category: 'A' },
86
+ { state: 'Alaska', value: 200, category: 'B' },
87
+ { state: 'Arizona', value: 150, category: 'A' }
88
+ ],
89
+ legend: {},
90
+ newViz: true,
91
+ theme: 'theme-blue',
92
+ type: 'markup-include',
93
+ visual: {
94
+ border: false,
95
+ accent: false,
96
+ background: false,
97
+ hideBackgroundColor: false,
98
+ borderColorTheme: false
99
+ },
100
+ markupVariables: [
101
+ {
102
+ name: 'test_variable',
103
+ columnName: 'value',
104
+ tag: '{{test_variable}}',
105
+ conditions: [],
106
+ addCommas: false
107
+ }
108
+ ],
109
+ enableMarkupVariables: false,
110
+ version: '1.0.0'
111
+ }
112
+
113
+ // ============================================================================
114
+ // GENERAL SECTION TESTS
115
+ // Tests title input and display
116
+ // ============================================================================
117
+ export const GeneralSectionTests: Story = {
118
+ args: {
119
+ config: testConfig as any,
120
+ isEditor: true
121
+ },
122
+ play: async ({ canvasElement }) => {
123
+ const canvas = within(canvasElement)
124
+ await waitForEditor(canvas)
125
+ await openAccordion(canvas, 'General')
126
+
127
+ // ============================================================================
128
+ // TEST 1: Title Update
129
+ // Expectation: Header text updates to new string and becomes visible
130
+ // ============================================================================
131
+ const titleInput = canvasElement.querySelector('input[name*="title"]') as HTMLInputElement
132
+ expect(titleInput).toBeTruthy()
133
+ expect(titleInput.value).toBe('Test Markup Include Title')
134
+
135
+ await performAndAssert(
136
+ 'Title Update',
137
+ () => {
138
+ const headerElement = canvasElement.querySelector('.cove-component__header h2')
139
+ return headerElement?.textContent?.trim() || ''
140
+ },
141
+ async () => {
142
+ await userEvent.clear(titleInput)
143
+ await userEvent.type(titleInput, 'Updated Markup Include Title E2E')
144
+ },
145
+ (before, after) => after === 'Updated Markup Include Title E2E' && after !== before
146
+ )
147
+
148
+ const headerElement = canvasElement.querySelector('.cove-component__header h2')
149
+ expect(headerElement).toBeTruthy()
150
+ expect(headerElement!.textContent?.trim()).toBe('Updated Markup Include Title E2E')
151
+ }
152
+ }
153
+
154
+ // ============================================================================
155
+ // CONTENT EDITOR SECTION TESTS
156
+ // Tests ALL content editing functionality: basic controls, variables, and advanced features
157
+ // ============================================================================
158
+ export const ContentEditorTests: Story = {
159
+ args: {
160
+ config: testConfig as any,
161
+ isEditor: true
162
+ },
163
+ play: async ({ canvasElement }) => {
164
+ const canvas = within(canvasElement)
165
+ await waitForEditor(canvas)
166
+ await openAccordion(canvas, 'Content Editor')
167
+
168
+ // ============================================================================
169
+ // TEST 1: Use Inline HTML Toggle
170
+ // Expectation: Switching between inline HTML textarea and source URL input
171
+ // ============================================================================
172
+ const inlineHTMLCheckbox = canvasElement.querySelector('input[name*="useInlineHTML"]') as HTMLInputElement
173
+ expect(inlineHTMLCheckbox).toBeTruthy()
174
+ expect(inlineHTMLCheckbox.checked).toBe(true) // Should start as checked based on config
175
+
176
+ // Verify initial state - HTML textarea should be visible
177
+ let htmlTextarea = canvasElement.querySelector('textarea[name*="inlineHTML"]') as HTMLTextAreaElement
178
+ let srcUrlInput = canvasElement.querySelector('input[name*="srcUrl"]') as HTMLInputElement
179
+ expect(htmlTextarea).toBeTruthy()
180
+ expect(srcUrlInput).toBeFalsy() // Should not be visible when inline HTML is enabled
181
+
182
+ // Toggle to disable inline HTML (should show source URL input)
183
+ await performAndAssert(
184
+ 'Toggle to Source URL Mode',
185
+ () => ({
186
+ hasTextarea: !!canvasElement.querySelector('textarea[name*="inlineHTML"]'),
187
+ hasSrcInput: !!canvasElement.querySelector('input[name*="srcUrl"]'),
188
+ checkboxChecked: inlineHTMLCheckbox.checked
189
+ }),
190
+ async () => {
191
+ // Click the checkbox wrapper to handle pointer-events issues
192
+ const checkboxWrapper = inlineHTMLCheckbox.closest('.cove-input__checkbox--small') || inlineHTMLCheckbox
193
+ await userEvent.click(checkboxWrapper as HTMLElement)
194
+ },
195
+ (before, after) =>
196
+ before.checkboxChecked !== after.checkboxChecked &&
197
+ before.hasTextarea !== after.hasTextarea &&
198
+ before.hasSrcInput !== after.hasSrcInput
199
+ )
200
+
201
+ // Verify textarea is gone and source URL input is visible
202
+ htmlTextarea = canvasElement.querySelector('textarea[name*="inlineHTML"]') as HTMLTextAreaElement
203
+ srcUrlInput = canvasElement.querySelector('input[name*="srcUrl"]') as HTMLInputElement
204
+ expect(htmlTextarea).toBeFalsy()
205
+ expect(srcUrlInput).toBeTruthy()
206
+
207
+ // Toggle back to inline HTML mode
208
+ const checkboxWrapper2 = inlineHTMLCheckbox.closest('.cove-input__checkbox--small') || inlineHTMLCheckbox
209
+ await userEvent.click(checkboxWrapper2 as HTMLElement)
210
+ await waitForPresence('textarea[name*="inlineHTML"]', canvasElement)
211
+
212
+ htmlTextarea = canvasElement.querySelector('textarea[name*="inlineHTML"]') as HTMLTextAreaElement
213
+ expect(htmlTextarea).toBeTruthy()
214
+ expect(htmlTextarea.value).toBe('<h2>Test Markup Include</h2><p>{{test_variable}}</p>')
215
+
216
+ // ============================================================================
217
+ // TEST 2: HTML Content Update
218
+ // Expectation: Changes to HTML textarea affect the rendered output
219
+ // ============================================================================
220
+ await performAndAssert(
221
+ 'HTML Content Update',
222
+ () => {
223
+ const contentElement = canvasElement.querySelector('.cove-component__content')
224
+ return contentElement?.innerHTML || ''
225
+ },
226
+ async () => {
227
+ await userEvent.clear(htmlTextarea)
228
+ await userEvent.type(
229
+ htmlTextarea,
230
+ '<h3>Updated HTML Content</h3><p>New test content with {{{{test_variable}}</p>'
231
+ )
232
+ },
233
+ (before, after) => after !== before && after.includes('Updated HTML Content')
234
+ )
235
+
236
+ // ============================================================================
237
+ // TEST 3: Source URL Input (when in URL mode)
238
+ // Expectation: Source URL input accepts and stores values
239
+ // ============================================================================
240
+ // Switch back to source URL mode
241
+ const checkboxWrapper3 = inlineHTMLCheckbox.closest('.cove-input__checkbox--small') || inlineHTMLCheckbox
242
+ await userEvent.click(checkboxWrapper3 as HTMLElement)
243
+ await waitForPresence('input[name*="srcUrl"]', canvasElement)
244
+
245
+ srcUrlInput = canvasElement.querySelector('input[name*="srcUrl"]') as HTMLInputElement
246
+ expect(srcUrlInput).toBeTruthy()
247
+
248
+ await performAndAssert(
249
+ 'Source URL Update and Content Loading',
250
+ () => ({
251
+ inputValue: srcUrlInput.value,
252
+ contentText: canvasElement.querySelector('.cove-component__content')?.textContent || ''
253
+ }),
254
+ async () => {
255
+ await userEvent.clear(srcUrlInput)
256
+ await userEvent.type(
257
+ srcUrlInput,
258
+ 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/examples/Markup-Include-Button-and-Text.html'
259
+ )
260
+ },
261
+ (before, after) => {
262
+ const expectedUrl =
263
+ 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/examples/Markup-Include-Button-and-Text.html'
264
+ return (
265
+ after.inputValue === expectedUrl &&
266
+ after.inputValue !== before.inputValue &&
267
+ after.contentText.includes('Neque laoreet')
268
+ )
269
+ }
270
+ )
271
+
272
+ // Switch back to inline mode for subsequent tests
273
+ const checkboxWrapper4 = inlineHTMLCheckbox.closest('.cove-input__checkbox--small') || inlineHTMLCheckbox
274
+ await userEvent.click(checkboxWrapper4 as HTMLElement)
275
+ await waitForPresence('textarea[name*="inlineHTML"]', canvasElement)
276
+ }
277
+ }
278
+
279
+ // ============================================================================
280
+ // VISUAL SECTION TESTS
281
+ // Tests theme and visual styling controls
282
+ // ============================================================================
283
+ export const VisualSectionTests: Story = {
284
+ args: {
285
+ config: testConfig as any,
286
+ isEditor: true
287
+ },
288
+ play: async ({ canvasElement }) => {
289
+ const canvas = within(canvasElement)
290
+ await waitForEditor(canvas)
291
+ await openAccordion(canvas, 'Visual')
292
+
293
+ const contentContainer = () => canvasElement.querySelector('.cove-component__content') as HTMLElement
294
+ const visualContainer = () => canvasElement.querySelector('.markup-include-component') as HTMLElement
295
+ expect(contentContainer()).toBeTruthy()
296
+ expect(visualContainer()).toBeTruthy()
297
+
298
+ // ============================================================================
299
+ // TEST 1: Theme Palette Change
300
+ // Expectation: Theme class changes on component
301
+ // ============================================================================
302
+ const getThemeState = () => {
303
+ // Use the contentContainer like other tests, and check its parent for theme classes
304
+ const content = contentContainer()
305
+ if (!content) return { theme: '', classes: '', element: 'content not found' }
306
+
307
+ // Check content itself and its parent for theme classes
308
+ const contentClasses = Array.from(content.classList).join(' ')
309
+ const parentClasses = content.parentElement ? Array.from(content.parentElement.classList).join(' ') : ''
310
+
311
+ const contentTheme = Array.from(content.classList).find(cls => cls.startsWith('theme-')) || ''
312
+ const parentTheme = content.parentElement
313
+ ? Array.from(content.parentElement.classList).find(cls => cls.startsWith('theme-')) || ''
314
+ : ''
315
+
316
+ const theme = contentTheme || parentTheme || ''
317
+
318
+ return {
319
+ theme,
320
+ classes: contentClasses + ' | parent: ' + parentClasses,
321
+ computedStyles: getComputedStyle(content).backgroundColor,
322
+ element: `${content.tagName.toLowerCase()} parent: ${content.parentElement?.tagName.toLowerCase() || 'none'}`
323
+ }
324
+ }
325
+
326
+ const themeButtons = Array.from(canvasElement.querySelectorAll('.color-palette button')) as HTMLElement[]
327
+ expect(themeButtons.length).toBeGreaterThan(1)
328
+
329
+ await performAndAssert(
330
+ 'Theme Change',
331
+ getThemeState,
332
+ async () => {
333
+ const selected = themeButtons.find(btn => btn.classList.contains('selected'))
334
+ const next = themeButtons.find(btn => !btn.classList.contains('selected')) || themeButtons[1]
335
+ await userEvent.click(next)
336
+ },
337
+ (before, after) => before.theme !== after.theme
338
+ )
339
+
340
+ // ============================================================================
341
+ // TEST 2: Display Border Toggle
342
+ // Expectation: Border styles change on content container
343
+ // ============================================================================
344
+ const borderCheckbox = canvasElement.querySelector(
345
+ 'input[name*="border"]:not([name*="borderColorTheme"])'
346
+ ) as HTMLInputElement
347
+ expect(borderCheckbox).toBeTruthy()
348
+
349
+ await performAndAssert(
350
+ 'Display Border Toggle',
351
+ () => ({
352
+ checked: borderCheckbox.checked,
353
+ hasBorders: !visualContainer().classList.contains('no-borders')
354
+ }),
355
+ async () => {
356
+ const checkboxWrapper = borderCheckbox.closest('.cove-input__checkbox--small') || borderCheckbox
357
+ await userEvent.click(checkboxWrapper as HTMLElement)
358
+ },
359
+ (before, after) => {
360
+ const checkboxChanged = before.checked !== after.checked
361
+ const borderChanged = before.hasBorders !== after.hasBorders
362
+ return checkboxChanged && borderChanged
363
+ }
364
+ )
365
+
366
+ // ============================================================================
367
+ // TEST 3: Border Color Theme Toggle
368
+ // Expectation: Border color theme class changes
369
+ // ============================================================================
370
+ const borderColorThemeCheckbox = canvasElement.querySelector('input[name*="borderColorTheme"]') as HTMLInputElement
371
+ expect(borderColorThemeCheckbox).toBeTruthy()
372
+
373
+ await performAndAssert(
374
+ 'Border Color Theme Toggle',
375
+ () => ({
376
+ checked: borderColorThemeCheckbox.checked,
377
+ hasBorderColorTheme: visualContainer().classList.contains('component--has-borderColorTheme')
378
+ }),
379
+ async () => {
380
+ const checkboxWrapper =
381
+ borderColorThemeCheckbox.closest('.cove-input__checkbox--small') || borderColorThemeCheckbox
382
+ await userEvent.click(checkboxWrapper as HTMLElement)
383
+ },
384
+ (before, after) => {
385
+ const checkboxChanged = before.checked !== after.checked
386
+ const themeChanged = before.hasBorderColorTheme !== after.hasBorderColorTheme
387
+ return checkboxChanged && themeChanged
388
+ }
389
+ )
390
+
391
+ // ============================================================================
392
+ // TEST 4: Accent Style Toggle
393
+ // Expectation: Accent class toggles
394
+ // ============================================================================
395
+ const accentCheckbox = canvasElement.querySelector('input[name*="accent"]') as HTMLInputElement
396
+ expect(accentCheckbox).toBeTruthy()
397
+
398
+ await performAndAssert(
399
+ 'Accent Style Toggle',
400
+ () => ({
401
+ checked: accentCheckbox.checked,
402
+ hasAccent: visualContainer().classList.contains('component--has-accent')
403
+ }),
404
+ async () => {
405
+ const checkboxWrapper = accentCheckbox.closest('.cove-input__checkbox--small') || accentCheckbox
406
+ await userEvent.click(checkboxWrapper as HTMLElement)
407
+ },
408
+ (before, after) => {
409
+ const checkboxChanged = before.checked !== after.checked
410
+ const accentChanged = before.hasAccent !== after.hasAccent
411
+ return checkboxChanged && accentChanged
412
+ }
413
+ )
414
+
415
+ // ============================================================================
416
+ // TEST 5: Theme Background Color Toggle
417
+ // Expectation: Background class toggles
418
+ // ============================================================================
419
+ const backgroundCheckbox = canvasElement.querySelector(
420
+ 'input[name*="background"]:not([name*="hide"])'
421
+ ) as HTMLInputElement
422
+ expect(backgroundCheckbox).toBeTruthy()
423
+
424
+ await performAndAssert(
425
+ 'Theme Background Color Toggle',
426
+ () => ({
427
+ checked: backgroundCheckbox.checked,
428
+ hasBackground: visualContainer().classList.contains('component--has-background')
429
+ }),
430
+ async () => {
431
+ const checkboxWrapper = backgroundCheckbox.closest('.cove-input__checkbox--small') || backgroundCheckbox
432
+ await userEvent.click(checkboxWrapper as HTMLElement)
433
+ },
434
+ (before, after) => {
435
+ const checkboxChanged = before.checked !== after.checked
436
+ const backgroundChanged = before.hasBackground !== after.hasBackground
437
+ return checkboxChanged && backgroundChanged
438
+ }
439
+ )
440
+
441
+ // ============================================================================
442
+ // TEST 6: Hide Background Color Toggle
443
+ // Expectation: Hide background class toggles
444
+ // ============================================================================
445
+ const hideBackgroundCheckbox = canvasElement.querySelector('input[name*="hideBackgroundColor"]') as HTMLInputElement
446
+ expect(hideBackgroundCheckbox).toBeTruthy()
447
+
448
+ await performAndAssert(
449
+ 'Hide Background Color Toggle',
450
+ () => ({
451
+ checked: hideBackgroundCheckbox.checked,
452
+ hideBackground: visualContainer().classList.contains('component--hideBackgroundColor')
453
+ }),
454
+ async () => {
455
+ const checkboxWrapper = hideBackgroundCheckbox.closest('.cove-input__checkbox--small') || hideBackgroundCheckbox
456
+ await userEvent.click(checkboxWrapper as HTMLElement)
457
+ },
458
+ (before, after) => {
459
+ const checkboxChanged = before.checked !== after.checked
460
+ const hideChanged = before.hideBackground !== after.hideBackground
461
+ return checkboxChanged && hideChanged
462
+ }
463
+ )
464
+ }
465
+ }
@@ -1,58 +1,58 @@
1
- import type { Meta, StoryObj } from '@storybook/react'
2
- import CdcMarkupInclude from '../CdcMarkupInclude'
3
- import primary from './_mock/primary.json'
4
- import noConditions from './_mock/no-conditions.json'
5
- import withConditions from './_mock/with-conditions.json'
6
- import buttonAndText from './_mock/button-and-text.json'
7
- import iconNoText from './_mock/icon-no-text.json'
8
- import imageWithText from './_mock/image-with-text.json'
9
-
10
- const meta: Meta<typeof CdcMarkupInclude> = {
11
- title: 'Components/Pages/Markup Include',
12
- component: CdcMarkupInclude
13
- }
14
-
15
- type Story = StoryObj<typeof CdcMarkupInclude>
16
-
17
- export const Primary: Story = {
18
- args: {
19
- config: primary,
20
- isEditor: false
21
- }
22
- }
23
-
24
- export const No_Conditions: Story = {
25
- args: {
26
- config: noConditions,
27
- isEditor: false
28
- }
29
- }
30
-
31
- export const With_conditions: Story = {
32
- args: {
33
- config: withConditions,
34
- isEditor: false
35
- }
36
- }
37
-
38
- export const Button_and_text: Story = {
39
- args: {
40
- config: buttonAndText,
41
- isEditor: false
42
- }
43
- }
44
-
45
- export const icon_no_text: Story = {
46
- args: {
47
- config: iconNoText,
48
- isEditor: false
49
- }
50
- }
51
- export const image_with_text: Story = {
52
- args: {
53
- config: imageWithText,
54
- isEditor: false
55
- }
56
- }
57
-
58
- export default meta
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import CdcMarkupInclude from '../CdcMarkupInclude'
3
+ import primary from './_mock/primary.json'
4
+ import noConditions from './_mock/no-conditions.json'
5
+ import withConditions from './_mock/with-conditions.json'
6
+ import buttonAndText from './_mock/button-and-text.json'
7
+ import iconNoText from './_mock/icon-no-text.json'
8
+ import imageWithText from './_mock/image-with-text.json'
9
+
10
+ const meta: Meta<typeof CdcMarkupInclude> = {
11
+ title: 'Components/Pages/Markup Include',
12
+ component: CdcMarkupInclude
13
+ }
14
+
15
+ type Story = StoryObj<typeof CdcMarkupInclude>
16
+
17
+ export const Primary: Story = {
18
+ args: {
19
+ config: primary,
20
+ isEditor: false
21
+ }
22
+ }
23
+
24
+ export const No_Conditions: Story = {
25
+ args: {
26
+ config: noConditions,
27
+ isEditor: false
28
+ }
29
+ }
30
+
31
+ export const With_conditions: Story = {
32
+ args: {
33
+ config: withConditions,
34
+ isEditor: false
35
+ }
36
+ }
37
+
38
+ export const Button_and_text: Story = {
39
+ args: {
40
+ config: buttonAndText,
41
+ isEditor: false
42
+ }
43
+ }
44
+
45
+ export const icon_no_text: Story = {
46
+ args: {
47
+ config: iconNoText,
48
+ isEditor: false
49
+ }
50
+ }
51
+ export const image_with_text: Story = {
52
+ args: {
53
+ config: imageWithText,
54
+ isEditor: false
55
+ }
56
+ }
57
+
58
+ export default meta