@capillarytech/creatives-library 8.0.208 → 8.0.209

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 (77) hide show
  1. package/assets/Android.png +0 -0
  2. package/assets/iOS.png +0 -0
  3. package/config/app.js +1 -2
  4. package/package.json +16 -2
  5. package/services/api.js +0 -2
  6. package/v2Components/HtmlEditor/HTMLEditor.js +508 -0
  7. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1809 -0
  8. package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +532 -0
  9. package/v2Components/HtmlEditor/_htmlEditor.scss +304 -0
  10. package/v2Components/HtmlEditor/_index.lazy.scss +26 -0
  11. package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +376 -0
  12. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +331 -0
  13. package/v2Components/HtmlEditor/components/DeviceToggle/__tests__/index.test.js +314 -0
  14. package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +244 -0
  15. package/v2Components/HtmlEditor/components/DeviceToggle/index.js +111 -0
  16. package/v2Components/HtmlEditor/components/EditorToolbar/PreviewModeGroup.js +72 -0
  17. package/v2Components/HtmlEditor/components/EditorToolbar/__tests__/PreviewModeGroup.test.js +1594 -0
  18. package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +113 -0
  19. package/v2Components/HtmlEditor/components/EditorToolbar/_previewModeGroup.scss +82 -0
  20. package/v2Components/HtmlEditor/components/EditorToolbar/index.js +115 -0
  21. package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +57 -0
  22. package/v2Components/HtmlEditor/components/InAppPreviewPane/ContentOverlay.js +90 -0
  23. package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +60 -0
  24. package/v2Components/HtmlEditor/components/InAppPreviewPane/LayoutSelector.js +58 -0
  25. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/ContentOverlay.test.js +389 -0
  26. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +424 -0
  27. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/LayoutSelector.test.js +248 -0
  28. package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +253 -0
  29. package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +104 -0
  30. package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +179 -0
  31. package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +220 -0
  32. package/v2Components/HtmlEditor/components/PreviewPane/index.js +229 -0
  33. package/v2Components/HtmlEditor/components/SplitContainer/SplitContainer.js +276 -0
  34. package/v2Components/HtmlEditor/components/SplitContainer/__tests__/SplitContainer.test.js +295 -0
  35. package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +257 -0
  36. package/v2Components/HtmlEditor/components/SplitContainer/index.js +7 -0
  37. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +152 -0
  38. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +31 -0
  39. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +70 -0
  40. package/v2Components/HtmlEditor/components/ValidationPanel/__tests__/index.test.js +98 -0
  41. package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +311 -0
  42. package/v2Components/HtmlEditor/components/ValidationPanel/index.js +297 -0
  43. package/v2Components/HtmlEditor/components/ValidationPanel/messages.js +57 -0
  44. package/v2Components/HtmlEditor/components/common/EditorContext.js +84 -0
  45. package/v2Components/HtmlEditor/components/common/__tests__/EditorContext.test.js +660 -0
  46. package/v2Components/HtmlEditor/constants.js +241 -0
  47. package/v2Components/HtmlEditor/hooks/__tests__/useEditorContent.test.js +450 -0
  48. package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +785 -0
  49. package/v2Components/HtmlEditor/hooks/__tests__/useLayoutState.test.js +580 -0
  50. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.enhanced.test.js +768 -0
  51. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +590 -0
  52. package/v2Components/HtmlEditor/hooks/useEditorContent.js +274 -0
  53. package/v2Components/HtmlEditor/hooks/useInAppContent.js +407 -0
  54. package/v2Components/HtmlEditor/hooks/useLayoutState.js +247 -0
  55. package/v2Components/HtmlEditor/hooks/useValidation.js +325 -0
  56. package/v2Components/HtmlEditor/index.js +29 -0
  57. package/v2Components/HtmlEditor/index.lazy.js +114 -0
  58. package/v2Components/HtmlEditor/messages.js +389 -0
  59. package/v2Components/HtmlEditor/utils/__tests__/contentSanitizer.test.js +741 -0
  60. package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +1042 -0
  61. package/v2Components/HtmlEditor/utils/__tests__/liquidTemplateSupport.test.js +515 -0
  62. package/v2Components/HtmlEditor/utils/__tests__/properSyntaxHighlighting.test.js +473 -0
  63. package/v2Components/HtmlEditor/utils/__tests__/simplePerformance.test.js +1109 -0
  64. package/v2Components/HtmlEditor/utils/__tests__/validationAdapter.test.js +240 -0
  65. package/v2Components/HtmlEditor/utils/contentSanitizer.js +433 -0
  66. package/v2Components/HtmlEditor/utils/htmlValidator.js +508 -0
  67. package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +524 -0
  68. package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +163 -0
  69. package/v2Components/HtmlEditor/utils/simplePerformance.js +145 -0
  70. package/v2Components/HtmlEditor/utils/validationAdapter.js +130 -0
  71. package/v2Containers/CreativesContainer/SlideBoxContent.js +0 -2
  72. package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +200 -0
  73. package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +545 -0
  74. package/v2Containers/EmailWrapper/index.js +8 -1
  75. package/v2Containers/Templates/constants.js +8 -0
  76. package/v2Containers/Templates/index.js +56 -28
  77. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +5 -14
@@ -0,0 +1,424 @@
1
+ /**
2
+ * DeviceFrame Tests
3
+ *
4
+ * Tests for the DeviceFrame component
5
+ */
6
+
7
+ import React from 'react';
8
+ import { render, screen } from '@testing-library/react';
9
+ import '@testing-library/jest-dom';
10
+ import { DEVICE_TYPES } from '../../../constants';
11
+
12
+ // Mock the entire DeviceFrame module to bypass image import issues
13
+ jest.mock('../../../../assets/Android.png', () => 'mocked-android-frame.png');
14
+ jest.mock('../../../../assets/iOS.png', () => 'mocked-ios-frame.png');
15
+
16
+ // Import after mocking
17
+ import DeviceFrame from '../DeviceFrame';
18
+
19
+ describe('DeviceFrame', () => {
20
+ describe('Basic Rendering', () => {
21
+ it('renders without crashing', () => {
22
+ const { container } = render(<DeviceFrame />);
23
+ expect(container.firstChild).toBeInTheDocument();
24
+ });
25
+
26
+ it('renders with default props', () => {
27
+ const { container } = render(<DeviceFrame />);
28
+ const frame = container.firstChild;
29
+
30
+ expect(frame).toHaveClass('device-frame');
31
+ expect(frame).toHaveClass('device-frame--android'); // Default device
32
+ });
33
+
34
+ it('renders children correctly', () => {
35
+ render(
36
+ <DeviceFrame>
37
+ <div data-testid="child-content">Test Content</div>
38
+ </DeviceFrame>
39
+ );
40
+
41
+ expect(screen.getByTestId('child-content')).toBeInTheDocument();
42
+ expect(screen.getByTestId('child-content')).toHaveTextContent('Test Content');
43
+ });
44
+
45
+ it('renders multiple children', () => {
46
+ render(
47
+ <DeviceFrame>
48
+ <div data-testid="child-1">Child 1</div>
49
+ <div data-testid="child-2">Child 2</div>
50
+ <div data-testid="child-3">Child 3</div>
51
+ </DeviceFrame>
52
+ );
53
+
54
+ expect(screen.getByTestId('child-1')).toBeInTheDocument();
55
+ expect(screen.getByTestId('child-2')).toBeInTheDocument();
56
+ expect(screen.getByTestId('child-3')).toBeInTheDocument();
57
+ });
58
+ });
59
+
60
+ describe('Device Types', () => {
61
+ it('renders Android frame correctly', () => {
62
+ const { container } = render(<DeviceFrame device={DEVICE_TYPES.ANDROID} />);
63
+ const frame = container.firstChild;
64
+
65
+ expect(frame).toHaveClass('device-frame--android');
66
+ expect(frame.style.backgroundImage).toBeTruthy();
67
+ expect(frame.style.backgroundImage).toMatch(/url\(/);
68
+ });
69
+
70
+ it('renders iOS frame correctly', () => {
71
+ const { container } = render(<DeviceFrame device={DEVICE_TYPES.IOS} />);
72
+ const frame = container.firstChild;
73
+
74
+ expect(frame).toHaveClass('device-frame--ios');
75
+ expect(frame.style.backgroundImage).toBeTruthy();
76
+ expect(frame.style.backgroundImage).toMatch(/url\(/);
77
+ });
78
+
79
+ it('defaults to Android when no device specified', () => {
80
+ const { container } = render(<DeviceFrame />);
81
+ const frame = container.firstChild;
82
+
83
+ expect(frame).toHaveClass('device-frame--android');
84
+ expect(frame.style.backgroundImage).toBeTruthy();
85
+ expect(frame.style.backgroundImage).toMatch(/url\(/);
86
+ });
87
+
88
+ it('handles device type case insensitivity', () => {
89
+ const { container: androidContainer } = render(
90
+ <DeviceFrame device={DEVICE_TYPES.ANDROID} />
91
+ );
92
+ expect(androidContainer.firstChild).toHaveClass('device-frame--android');
93
+
94
+ const { container: iosContainer } = render(
95
+ <DeviceFrame device={DEVICE_TYPES.IOS} />
96
+ );
97
+ expect(iosContainer.firstChild).toHaveClass('device-frame--ios');
98
+ });
99
+ });
100
+
101
+ describe('Styling', () => {
102
+ it('applies correct dimensions', () => {
103
+ const { container } = render(<DeviceFrame />);
104
+ const frame = container.firstChild;
105
+
106
+ expect(frame.style.width).toBe('450px');
107
+ expect(frame.style.height).toBe('920px');
108
+ });
109
+
110
+ it('applies unified dimensions for both devices', () => {
111
+ const { container: androidContainer } = render(
112
+ <DeviceFrame device={DEVICE_TYPES.ANDROID} />
113
+ );
114
+ const androidFrame = androidContainer.firstChild;
115
+
116
+ const { container: iosContainer } = render(
117
+ <DeviceFrame device={DEVICE_TYPES.IOS} />
118
+ );
119
+ const iosFrame = iosContainer.firstChild;
120
+
121
+ expect(androidFrame.style.width).toBe(iosFrame.style.width);
122
+ expect(androidFrame.style.height).toBe(iosFrame.style.height);
123
+ });
124
+
125
+ it('applies background styles correctly', () => {
126
+ const { container } = render(<DeviceFrame />);
127
+ const frame = container.firstChild;
128
+
129
+ expect(frame.style.position).toBe('relative');
130
+ expect(frame.style.display).toBe('inline-block');
131
+ expect(frame.style.backgroundRepeat).toBe('no-repeat');
132
+ expect(frame.style.backgroundPosition).toBe('center');
133
+ expect(frame.style.backgroundSize).toBe('contain');
134
+ });
135
+
136
+ it('applies drop-shadow filter', () => {
137
+ const { container } = render(<DeviceFrame />);
138
+ const frame = container.firstChild;
139
+
140
+ expect(frame.style.filter).toContain('drop-shadow');
141
+ expect(frame.style.filter).toContain('brightness');
142
+ expect(frame.style.filter).toContain('contrast');
143
+ });
144
+
145
+ it('sets correct background image for Android', () => {
146
+ const { container } = render(<DeviceFrame device={DEVICE_TYPES.ANDROID} />);
147
+ const frame = container.firstChild;
148
+
149
+ // Verify background image is set (actual mock value may vary)
150
+ expect(frame.style.backgroundImage).toBeTruthy();
151
+ expect(frame.style.backgroundImage).toMatch(/^url\(.+\)$/);
152
+ });
153
+
154
+ it('sets correct background image for iOS', () => {
155
+ const { container } = render(<DeviceFrame device={DEVICE_TYPES.IOS} />);
156
+ const frame = container.firstChild;
157
+
158
+ // Verify background image is set (actual mock value may vary)
159
+ expect(frame.style.backgroundImage).toBeTruthy();
160
+ expect(frame.style.backgroundImage).toMatch(/^url\(.+\)$/);
161
+ });
162
+ });
163
+
164
+ describe('Custom Props', () => {
165
+ it('accepts and applies custom className', () => {
166
+ const { container } = render(<DeviceFrame className="custom-frame" />);
167
+ const frame = container.firstChild;
168
+
169
+ expect(frame).toHaveClass('custom-frame');
170
+ expect(frame).toHaveClass('device-frame'); // Base class still present
171
+ });
172
+
173
+ it('merges multiple classNames correctly', () => {
174
+ const { container } = render(
175
+ <DeviceFrame className="custom-class another-class" />
176
+ );
177
+ const frame = container.firstChild;
178
+
179
+ expect(frame).toHaveClass('custom-class');
180
+ expect(frame).toHaveClass('another-class');
181
+ expect(frame).toHaveClass('device-frame');
182
+ });
183
+
184
+ it('forwards additional props to container', () => {
185
+ const { container } = render(
186
+ <DeviceFrame data-testid="custom-frame" aria-label="Device Preview" />
187
+ );
188
+ const frame = container.firstChild;
189
+
190
+ expect(frame).toHaveAttribute('data-testid', 'custom-frame');
191
+ expect(frame).toHaveAttribute('aria-label', 'Device Preview');
192
+ });
193
+
194
+ it('handles onClick event', () => {
195
+ const handleClick = jest.fn();
196
+ const { container } = render(<DeviceFrame onClick={handleClick} />);
197
+ const frame = container.firstChild;
198
+
199
+ frame.click();
200
+ expect(handleClick).toHaveBeenCalledTimes(1);
201
+ });
202
+
203
+ it('handles custom styles via props', () => {
204
+ const customStyle = { border: '2px solid red' };
205
+ const { container } = render(<DeviceFrame style={customStyle} />);
206
+ const frame = container.firstChild;
207
+
208
+ // Note: inline styles from props override component styles
209
+ expect(frame.style.border).toBe('2px solid red');
210
+ });
211
+ });
212
+
213
+ describe('PropTypes Validation', () => {
214
+ // Suppress console errors for these tests
215
+ const originalError = console.error;
216
+ beforeAll(() => {
217
+ console.error = jest.fn();
218
+ });
219
+ afterAll(() => {
220
+ console.error = originalError;
221
+ });
222
+
223
+ it('accepts valid device types', () => {
224
+ expect(() => {
225
+ render(<DeviceFrame device={DEVICE_TYPES.ANDROID} />);
226
+ }).not.toThrow();
227
+
228
+ expect(() => {
229
+ render(<DeviceFrame device={DEVICE_TYPES.IOS} />);
230
+ }).not.toThrow();
231
+ });
232
+
233
+ it('accepts string className', () => {
234
+ expect(() => {
235
+ render(<DeviceFrame className="test-class" />);
236
+ }).not.toThrow();
237
+ });
238
+
239
+ it('accepts valid children types', () => {
240
+ expect(() => {
241
+ render(<DeviceFrame><div>Test</div></DeviceFrame>);
242
+ }).not.toThrow();
243
+
244
+ expect(() => {
245
+ render(<DeviceFrame>Plain text</DeviceFrame>);
246
+ }).not.toThrow();
247
+
248
+ expect(() => {
249
+ render(<DeviceFrame>{null}</DeviceFrame>);
250
+ }).not.toThrow();
251
+ });
252
+ });
253
+
254
+ describe('Edge Cases', () => {
255
+ it('renders without children', () => {
256
+ const { container } = render(<DeviceFrame />);
257
+ expect(container.firstChild).toBeInTheDocument();
258
+ });
259
+
260
+ it('renders with null children', () => {
261
+ const { container } = render(<DeviceFrame>{null}</DeviceFrame>);
262
+ expect(container.firstChild).toBeInTheDocument();
263
+ });
264
+
265
+ it('renders with undefined children', () => {
266
+ const { container } = render(<DeviceFrame>{undefined}</DeviceFrame>);
267
+ expect(container.firstChild).toBeInTheDocument();
268
+ });
269
+
270
+ it('renders with empty string className', () => {
271
+ const { container } = render(<DeviceFrame className="" />);
272
+ const frame = container.firstChild;
273
+
274
+ expect(frame).toHaveClass('device-frame');
275
+ });
276
+
277
+ it('renders with fragment children', () => {
278
+ render(
279
+ <DeviceFrame>
280
+ <>
281
+ <div data-testid="fragment-child-1">Child 1</div>
282
+ <div data-testid="fragment-child-2">Child 2</div>
283
+ </>
284
+ </DeviceFrame>
285
+ );
286
+
287
+ expect(screen.getByTestId('fragment-child-1')).toBeInTheDocument();
288
+ expect(screen.getByTestId('fragment-child-2')).toBeInTheDocument();
289
+ });
290
+
291
+ it('handles complex nested children', () => {
292
+ render(
293
+ <DeviceFrame>
294
+ <div>
295
+ <div>
296
+ <div data-testid="nested-child">Deeply Nested</div>
297
+ </div>
298
+ </div>
299
+ </DeviceFrame>
300
+ );
301
+
302
+ expect(screen.getByTestId('nested-child')).toBeInTheDocument();
303
+ });
304
+ });
305
+
306
+ describe('Component Structure', () => {
307
+ it('renders a single root div element', () => {
308
+ const { container } = render(<DeviceFrame />);
309
+
310
+ expect(container.firstChild.tagName).toBe('DIV');
311
+ expect(container.children).toHaveLength(1);
312
+ });
313
+
314
+ it('maintains consistent class structure', () => {
315
+ const { container: androidContainer } = render(
316
+ <DeviceFrame device={DEVICE_TYPES.ANDROID} className="custom" />
317
+ );
318
+ const androidFrame = androidContainer.firstChild;
319
+
320
+ const classes = androidFrame.className.split(' ').filter(c => c);
321
+ expect(classes).toContain('device-frame');
322
+ expect(classes).toContain('device-frame--android');
323
+ expect(classes).toContain('custom');
324
+ });
325
+
326
+ it('applies styles as inline styles object', () => {
327
+ const { container } = render(<DeviceFrame />);
328
+ const frame = container.firstChild;
329
+
330
+ // Verify style is an object
331
+ expect(typeof frame.style).toBe('object');
332
+ expect(frame.style).not.toBeNull();
333
+ });
334
+ });
335
+
336
+ describe('Accessibility', () => {
337
+ it('can receive accessibility props', () => {
338
+ const { container } = render(
339
+ <DeviceFrame
340
+ role="img"
341
+ aria-label="Mobile Device Preview"
342
+ aria-describedby="device-description"
343
+ />
344
+ );
345
+ const frame = container.firstChild;
346
+
347
+ expect(frame).toHaveAttribute('role', 'img');
348
+ expect(frame).toHaveAttribute('aria-label', 'Mobile Device Preview');
349
+ expect(frame).toHaveAttribute('aria-describedby', 'device-description');
350
+ });
351
+
352
+ it('allows custom data attributes', () => {
353
+ const { container } = render(
354
+ <DeviceFrame
355
+ data-device="android"
356
+ data-testid="device-frame"
357
+ data-analytics="preview-frame"
358
+ />
359
+ );
360
+ const frame = container.firstChild;
361
+
362
+ expect(frame).toHaveAttribute('data-device', 'android');
363
+ expect(frame).toHaveAttribute('data-testid', 'device-frame');
364
+ expect(frame).toHaveAttribute('data-analytics', 'preview-frame');
365
+ });
366
+ });
367
+
368
+ describe('Integration Scenarios', () => {
369
+ it('works with ContentOverlay-style content', () => {
370
+ render(
371
+ <DeviceFrame device={DEVICE_TYPES.IOS}>
372
+ <div style={{ width: '100%', height: '100%' }}>
373
+ <iframe
374
+ title="preview"
375
+ srcDoc="<p>Preview Content</p>"
376
+ data-testid="preview-iframe"
377
+ />
378
+ </div>
379
+ </DeviceFrame>
380
+ );
381
+
382
+ expect(screen.getByTestId('preview-iframe')).toBeInTheDocument();
383
+ });
384
+
385
+ it('maintains structure when device changes', () => {
386
+ const { container, rerender } = render(
387
+ <DeviceFrame device={DEVICE_TYPES.ANDROID}>
388
+ <div data-testid="content">Content</div>
389
+ </DeviceFrame>
390
+ );
391
+
392
+ expect(container.firstChild).toHaveClass('device-frame--android');
393
+ expect(screen.getByTestId('content')).toBeInTheDocument();
394
+
395
+ rerender(
396
+ <DeviceFrame device={DEVICE_TYPES.IOS}>
397
+ <div data-testid="content">Content</div>
398
+ </DeviceFrame>
399
+ );
400
+
401
+ expect(container.firstChild).toHaveClass('device-frame--ios');
402
+ expect(screen.getByTestId('content')).toBeInTheDocument();
403
+ });
404
+
405
+ it('handles dynamic children updates', () => {
406
+ const { rerender } = render(
407
+ <DeviceFrame>
408
+ <div data-testid="dynamic-content">Initial</div>
409
+ </DeviceFrame>
410
+ );
411
+
412
+ expect(screen.getByTestId('dynamic-content')).toHaveTextContent('Initial');
413
+
414
+ rerender(
415
+ <DeviceFrame>
416
+ <div data-testid="dynamic-content">Updated</div>
417
+ </DeviceFrame>
418
+ );
419
+
420
+ expect(screen.getByTestId('dynamic-content')).toHaveTextContent('Updated');
421
+ });
422
+ });
423
+ });
424
+
@@ -0,0 +1,248 @@
1
+ /**
2
+ * LayoutSelector Component Tests
3
+ *
4
+ * Tests for the LayoutSelector component that provides layout type selection.
5
+ */
6
+
7
+ import React from 'react';
8
+ import { render, screen, fireEvent, within } from '@testing-library/react';
9
+ import '@testing-library/jest-dom';
10
+ import LayoutSelector from '../LayoutSelector';
11
+ import { LAYOUT_TYPES, LAYOUT_OPTIONS } from '../constants';
12
+
13
+ describe('LayoutSelector', () => {
14
+ const defaultProps = {
15
+ value: LAYOUT_TYPES.MODAL,
16
+ onChange: jest.fn()
17
+ };
18
+
19
+ beforeEach(() => {
20
+ jest.clearAllMocks();
21
+ });
22
+
23
+ describe('Rendering', () => {
24
+ it('renders without crashing', () => {
25
+ const { container } = render(<LayoutSelector {...defaultProps} />);
26
+ expect(container.querySelector('.layout-selector')).toBeInTheDocument();
27
+ });
28
+
29
+ it('renders all layout option labels', () => {
30
+ render(<LayoutSelector {...defaultProps} />);
31
+
32
+ expect(screen.getByText('Modal')).toBeInTheDocument();
33
+ expect(screen.getByText('Top Banner')).toBeInTheDocument();
34
+ expect(screen.getByText('Bottom Banner')).toBeInTheDocument();
35
+ expect(screen.getByText('Fullscreen')).toBeInTheDocument();
36
+ });
37
+
38
+ it('renders with custom className', () => {
39
+ const { container } = render(
40
+ <LayoutSelector {...defaultProps} className="custom-layout-selector" />
41
+ );
42
+
43
+ const selector = container.querySelector('.layout-selector');
44
+ expect(selector).toHaveClass('custom-layout-selector');
45
+ });
46
+
47
+ it('applies base layout-selector class', () => {
48
+ const { container } = render(<LayoutSelector {...defaultProps} />);
49
+ expect(container.querySelector('.layout-selector')).toBeInTheDocument();
50
+ });
51
+
52
+ it('renders radio group with proper structure', () => {
53
+ const { container } = render(<LayoutSelector {...defaultProps} />);
54
+
55
+ const radioGroup = container.querySelector('.layout-selector__radio-group');
56
+ expect(radioGroup).toBeInTheDocument();
57
+ });
58
+ });
59
+
60
+ describe('Value Selection', () => {
61
+ it('renders with MODAL value by default', () => {
62
+ render(<LayoutSelector onChange={defaultProps.onChange} />);
63
+
64
+ // Component should render without error
65
+ expect(screen.getByText('Modal')).toBeInTheDocument();
66
+ });
67
+
68
+ it('accepts HEADER value', () => {
69
+ const { container } = render(
70
+ <LayoutSelector value={LAYOUT_TYPES.HEADER} onChange={defaultProps.onChange} />
71
+ );
72
+
73
+ expect(container.querySelector('.layout-selector')).toBeInTheDocument();
74
+ });
75
+
76
+ it('accepts FOOTER value', () => {
77
+ const { container } = render(
78
+ <LayoutSelector value={LAYOUT_TYPES.FOOTER} onChange={defaultProps.onChange} />
79
+ );
80
+
81
+ expect(container.querySelector('.layout-selector')).toBeInTheDocument();
82
+ });
83
+
84
+ it('accepts FULLSCREEN value', () => {
85
+ const { container } = render(
86
+ <LayoutSelector value={LAYOUT_TYPES.FULLSCREEN} onChange={defaultProps.onChange} />
87
+ );
88
+
89
+ expect(container.querySelector('.layout-selector')).toBeInTheDocument();
90
+ });
91
+ });
92
+
93
+ describe('User Interactions', () => {
94
+ it('calls onChange when a radio button is clicked', () => {
95
+ const handleChange = jest.fn();
96
+ const { container } = render(
97
+ <LayoutSelector value={LAYOUT_TYPES.MODAL} onChange={handleChange} />
98
+ );
99
+
100
+ // Find all radio inputs in the component
101
+ const radioInputs = container.querySelectorAll('input[type="radio"]');
102
+
103
+ // Should have 4 radio buttons (one for each layout type)
104
+ expect(radioInputs.length).toBe(4);
105
+
106
+ // Click the second radio button (Header/Top Banner)
107
+ if (radioInputs[1]) {
108
+ fireEvent.click(radioInputs[1]);
109
+ expect(handleChange).toHaveBeenCalled();
110
+ }
111
+ });
112
+
113
+ it('handles missing onChange gracefully', () => {
114
+ // Should not throw error when onChange is not provided
115
+ expect(() => {
116
+ render(<LayoutSelector value={LAYOUT_TYPES.MODAL} />);
117
+ }).not.toThrow();
118
+ });
119
+ });
120
+
121
+ describe('Layout Options', () => {
122
+ it('renders correct number of options', () => {
123
+ const { container } = render(<LayoutSelector {...defaultProps} />);
124
+
125
+ // Should render 4 layout options
126
+ const radioInputs = container.querySelectorAll('input[type="radio"]');
127
+ expect(radioInputs.length).toBe(LAYOUT_OPTIONS.length);
128
+ });
129
+
130
+ it('renders each layout option label', () => {
131
+ render(<LayoutSelector {...defaultProps} />);
132
+
133
+ LAYOUT_OPTIONS.forEach(option => {
134
+ expect(screen.getByText(option.label)).toBeInTheDocument();
135
+ });
136
+ });
137
+ });
138
+
139
+ describe('CSS Classes', () => {
140
+ it('applies radio group class', () => {
141
+ const { container } = render(<LayoutSelector {...defaultProps} />);
142
+
143
+ const radioGroup = container.querySelector('.layout-selector__radio-group');
144
+ expect(radioGroup).toBeInTheDocument();
145
+ });
146
+
147
+ it('combines custom className with base class', () => {
148
+ const { container } = render(
149
+ <LayoutSelector {...defaultProps} className="my-custom-class" />
150
+ );
151
+
152
+ const selector = container.querySelector('.layout-selector');
153
+ expect(selector).toHaveClass('layout-selector');
154
+ expect(selector).toHaveClass('my-custom-class');
155
+ });
156
+ });
157
+
158
+ describe('Props Spreading', () => {
159
+ it('spreads additional props to container', () => {
160
+ const { container } = render(
161
+ <LayoutSelector
162
+ {...defaultProps}
163
+ data-custom-attr="test-value"
164
+ id="custom-id"
165
+ />
166
+ );
167
+
168
+ const selector = container.querySelector('.layout-selector');
169
+ expect(selector).toHaveAttribute('data-custom-attr', 'test-value');
170
+ expect(selector).toHaveAttribute('id', 'custom-id');
171
+ });
172
+ });
173
+
174
+ describe('PropTypes Validation', () => {
175
+ it('accepts all valid layout type values', () => {
176
+ const validValues = Object.values(LAYOUT_TYPES);
177
+
178
+ validValues.forEach(value => {
179
+ expect(() => {
180
+ render(<LayoutSelector value={value} onChange={defaultProps.onChange} />);
181
+ }).not.toThrow();
182
+ });
183
+ });
184
+
185
+ it('accepts all valid size values', () => {
186
+ const validSizes = ['small', 'middle', 'large'];
187
+
188
+ validSizes.forEach(size => {
189
+ expect(() => {
190
+ render(<LayoutSelector {...defaultProps} size={size} />);
191
+ }).not.toThrow();
192
+ });
193
+ });
194
+ });
195
+
196
+ describe('Integration', () => {
197
+ it('maintains structure across re-renders', () => {
198
+ const { container, rerender } = render(
199
+ <LayoutSelector value={LAYOUT_TYPES.MODAL} onChange={defaultProps.onChange} />
200
+ );
201
+
202
+ expect(container.querySelector('.layout-selector')).toBeInTheDocument();
203
+
204
+ rerender(<LayoutSelector value={LAYOUT_TYPES.HEADER} onChange={defaultProps.onChange} />);
205
+ expect(container.querySelector('.layout-selector')).toBeInTheDocument();
206
+
207
+ rerender(<LayoutSelector value={LAYOUT_TYPES.FULLSCREEN} onChange={defaultProps.onChange} />);
208
+ expect(container.querySelector('.layout-selector')).toBeInTheDocument();
209
+ });
210
+
211
+ it('works with controlled component pattern', () => {
212
+ const ControlledComponent = () => {
213
+ const [value, setValue] = React.useState(LAYOUT_TYPES.MODAL);
214
+
215
+ return (
216
+ <div>
217
+ <div data-testid="current-value">{value}</div>
218
+ <LayoutSelector value={value} onChange={setValue} />
219
+ </div>
220
+ );
221
+ };
222
+
223
+ render(<ControlledComponent />);
224
+
225
+ expect(screen.getByTestId('current-value')).toHaveTextContent(LAYOUT_TYPES.MODAL);
226
+ expect(screen.getByText('Modal')).toBeInTheDocument();
227
+ });
228
+ });
229
+
230
+ describe('Component Structure', () => {
231
+ it('renders with proper DOM hierarchy', () => {
232
+ const { container } = render(<LayoutSelector {...defaultProps} />);
233
+
234
+ const selector = container.querySelector('.layout-selector');
235
+ expect(selector).toBeInTheDocument();
236
+
237
+ const radioGroup = selector.querySelector('.layout-selector__radio-group');
238
+ expect(radioGroup).toBeInTheDocument();
239
+ });
240
+
241
+ it('contains radio inputs', () => {
242
+ const { container } = render(<LayoutSelector {...defaultProps} />);
243
+
244
+ const radioInputs = container.querySelectorAll('input[type="radio"]');
245
+ expect(radioInputs.length).toBeGreaterThan(0);
246
+ });
247
+ });
248
+ });