@capillarytech/creatives-library 8.0.207 → 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/package.json +16 -2
  4. package/v2Components/HtmlEditor/HTMLEditor.js +508 -0
  5. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1809 -0
  6. package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +532 -0
  7. package/v2Components/HtmlEditor/_htmlEditor.scss +304 -0
  8. package/v2Components/HtmlEditor/_index.lazy.scss +26 -0
  9. package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +376 -0
  10. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +331 -0
  11. package/v2Components/HtmlEditor/components/DeviceToggle/__tests__/index.test.js +314 -0
  12. package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +244 -0
  13. package/v2Components/HtmlEditor/components/DeviceToggle/index.js +111 -0
  14. package/v2Components/HtmlEditor/components/EditorToolbar/PreviewModeGroup.js +72 -0
  15. package/v2Components/HtmlEditor/components/EditorToolbar/__tests__/PreviewModeGroup.test.js +1594 -0
  16. package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +113 -0
  17. package/v2Components/HtmlEditor/components/EditorToolbar/_previewModeGroup.scss +82 -0
  18. package/v2Components/HtmlEditor/components/EditorToolbar/index.js +115 -0
  19. package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +57 -0
  20. package/v2Components/HtmlEditor/components/InAppPreviewPane/ContentOverlay.js +90 -0
  21. package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +60 -0
  22. package/v2Components/HtmlEditor/components/InAppPreviewPane/LayoutSelector.js +58 -0
  23. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/ContentOverlay.test.js +389 -0
  24. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +424 -0
  25. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/LayoutSelector.test.js +248 -0
  26. package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +253 -0
  27. package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +104 -0
  28. package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +179 -0
  29. package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +220 -0
  30. package/v2Components/HtmlEditor/components/PreviewPane/index.js +229 -0
  31. package/v2Components/HtmlEditor/components/SplitContainer/SplitContainer.js +276 -0
  32. package/v2Components/HtmlEditor/components/SplitContainer/__tests__/SplitContainer.test.js +295 -0
  33. package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +257 -0
  34. package/v2Components/HtmlEditor/components/SplitContainer/index.js +7 -0
  35. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +152 -0
  36. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +31 -0
  37. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +70 -0
  38. package/v2Components/HtmlEditor/components/ValidationPanel/__tests__/index.test.js +98 -0
  39. package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +311 -0
  40. package/v2Components/HtmlEditor/components/ValidationPanel/index.js +297 -0
  41. package/v2Components/HtmlEditor/components/ValidationPanel/messages.js +57 -0
  42. package/v2Components/HtmlEditor/components/common/EditorContext.js +84 -0
  43. package/v2Components/HtmlEditor/components/common/__tests__/EditorContext.test.js +660 -0
  44. package/v2Components/HtmlEditor/constants.js +241 -0
  45. package/v2Components/HtmlEditor/hooks/__tests__/useEditorContent.test.js +450 -0
  46. package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +785 -0
  47. package/v2Components/HtmlEditor/hooks/__tests__/useLayoutState.test.js +580 -0
  48. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.enhanced.test.js +768 -0
  49. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +590 -0
  50. package/v2Components/HtmlEditor/hooks/useEditorContent.js +274 -0
  51. package/v2Components/HtmlEditor/hooks/useInAppContent.js +407 -0
  52. package/v2Components/HtmlEditor/hooks/useLayoutState.js +247 -0
  53. package/v2Components/HtmlEditor/hooks/useValidation.js +325 -0
  54. package/v2Components/HtmlEditor/index.js +29 -0
  55. package/v2Components/HtmlEditor/index.lazy.js +114 -0
  56. package/v2Components/HtmlEditor/messages.js +389 -0
  57. package/v2Components/HtmlEditor/utils/__tests__/contentSanitizer.test.js +741 -0
  58. package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +1042 -0
  59. package/v2Components/HtmlEditor/utils/__tests__/liquidTemplateSupport.test.js +515 -0
  60. package/v2Components/HtmlEditor/utils/__tests__/properSyntaxHighlighting.test.js +473 -0
  61. package/v2Components/HtmlEditor/utils/__tests__/simplePerformance.test.js +1109 -0
  62. package/v2Components/HtmlEditor/utils/__tests__/validationAdapter.test.js +240 -0
  63. package/v2Components/HtmlEditor/utils/contentSanitizer.js +433 -0
  64. package/v2Components/HtmlEditor/utils/htmlValidator.js +508 -0
  65. package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +524 -0
  66. package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +163 -0
  67. package/v2Components/HtmlEditor/utils/simplePerformance.js +145 -0
  68. package/v2Components/HtmlEditor/utils/validationAdapter.js +130 -0
  69. package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +200 -0
  70. package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +545 -0
  71. package/v2Containers/EmailWrapper/index.js +8 -1
  72. package/v2Containers/Templates/constants.js +8 -0
  73. package/v2Containers/Templates/index.js +56 -28
  74. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +5 -14
  75. package/v2Containers/Whatsapp/constants.js +26 -2
  76. package/v2Containers/Whatsapp/index.js +4 -1
  77. package/v2Containers/Whatsapp/tests/index.test.js +460 -18
@@ -0,0 +1,1594 @@
1
+ /**
2
+ * PreviewModeGroup Tests
3
+ *
4
+ * Tests for the PreviewModeGroup component
5
+ */
6
+
7
+ import React from 'react';
8
+ import { render, screen, fireEvent } from '@testing-library/react';
9
+ import '@testing-library/jest-dom';
10
+ import { IntlProvider } from 'react-intl';
11
+ import PreviewModeGroup from '../PreviewModeGroup';
12
+ import { PREVIEW_MODES } from '../../../constants';
13
+
14
+ // Mock Cap UI components
15
+ jest.mock('@capillarytech/cap-ui-library/CapButton', () => {
16
+ return function MockCapButton({ children, onClick, disabled, className, type, value, 'aria-label': ariaLabel, 'aria-pressed': ariaPressed, ...rest }) {
17
+ return (
18
+ <button
19
+ onClick={onClick}
20
+ disabled={disabled}
21
+ className={className}
22
+ data-type={type}
23
+ value={value}
24
+ aria-label={ariaLabel}
25
+ aria-pressed={ariaPressed}
26
+ {...rest}
27
+ >
28
+ {children}
29
+ </button>
30
+ );
31
+ };
32
+ });
33
+
34
+ jest.mock('@capillarytech/cap-ui-library/CapIcon', () => {
35
+ return function MockCapIcon({ type, size }) {
36
+ return <span data-testid={`icon-${type}`} data-size={size}>{type}</span>;
37
+ };
38
+ });
39
+
40
+ // Test wrapper with IntlProvider
41
+ const renderWithIntl = (component) => {
42
+ return render(
43
+ <IntlProvider locale="en" messages={{}}>
44
+ {component}
45
+ </IntlProvider>
46
+ );
47
+ };
48
+
49
+ describe('PreviewModeGroup', () => {
50
+ const mockOnChange = jest.fn();
51
+
52
+ beforeEach(() => {
53
+ jest.clearAllMocks();
54
+ });
55
+
56
+ describe('Basic Rendering', () => {
57
+ it('renders without crashing', () => {
58
+ renderWithIntl(
59
+ <PreviewModeGroup
60
+ value={PREVIEW_MODES.DESKTOP}
61
+ onChange={mockOnChange}
62
+ />
63
+ );
64
+
65
+ expect(screen.getByTestId('icon-desktop')).toBeInTheDocument();
66
+ expect(screen.getByTestId('icon-mobile')).toBeInTheDocument();
67
+ });
68
+
69
+ it('renders both desktop and mobile buttons', () => {
70
+ renderWithIntl(
71
+ <PreviewModeGroup
72
+ value={PREVIEW_MODES.DESKTOP}
73
+ onChange={mockOnChange}
74
+ />
75
+ );
76
+
77
+ const buttons = screen.getAllByRole('button');
78
+ expect(buttons).toHaveLength(2);
79
+ });
80
+
81
+ it('renders desktop icon', () => {
82
+ renderWithIntl(
83
+ <PreviewModeGroup
84
+ value={PREVIEW_MODES.DESKTOP}
85
+ onChange={mockOnChange}
86
+ />
87
+ );
88
+
89
+ const desktopIcon = screen.getByTestId('icon-desktop');
90
+ expect(desktopIcon).toBeInTheDocument();
91
+ expect(desktopIcon).toHaveAttribute('data-size', 'm');
92
+ });
93
+
94
+ it('renders mobile icon', () => {
95
+ renderWithIntl(
96
+ <PreviewModeGroup
97
+ value={PREVIEW_MODES.DESKTOP}
98
+ onChange={mockOnChange}
99
+ />
100
+ );
101
+
102
+ const mobileIcon = screen.getByTestId('icon-mobile');
103
+ expect(mobileIcon).toBeInTheDocument();
104
+ expect(mobileIcon).toHaveAttribute('data-size', 'm');
105
+ });
106
+
107
+ it('renders with correct container structure', () => {
108
+ const { container } = renderWithIntl(
109
+ <PreviewModeGroup
110
+ value={PREVIEW_MODES.DESKTOP}
111
+ onChange={mockOnChange}
112
+ />
113
+ );
114
+
115
+ const previewGroup = container.querySelector('.preview-mode-group');
116
+ expect(previewGroup).toBeInTheDocument();
117
+ expect(previewGroup).toHaveClass('preview-mode-group');
118
+ });
119
+ });
120
+
121
+ describe('Active State', () => {
122
+ it('marks desktop button as active when desktop mode is selected', () => {
123
+ renderWithIntl(
124
+ <PreviewModeGroup
125
+ value={PREVIEW_MODES.DESKTOP}
126
+ onChange={mockOnChange}
127
+ />
128
+ );
129
+
130
+ const buttons = screen.getAllByRole('button');
131
+ const desktopButton = buttons[0];
132
+
133
+ expect(desktopButton).toHaveClass('preview-mode-group__btn--active');
134
+ });
135
+
136
+ it('marks mobile button as active when mobile mode is selected', () => {
137
+ renderWithIntl(
138
+ <PreviewModeGroup
139
+ value={PREVIEW_MODES.MOBILE}
140
+ onChange={mockOnChange}
141
+ />
142
+ );
143
+
144
+ const buttons = screen.getAllByRole('button');
145
+ const mobileButton = buttons[1];
146
+
147
+ expect(mobileButton).toHaveClass('preview-mode-group__btn--active');
148
+ });
149
+
150
+ it('only one button is active at a time', () => {
151
+ renderWithIntl(
152
+ <PreviewModeGroup
153
+ value={PREVIEW_MODES.DESKTOP}
154
+ onChange={mockOnChange}
155
+ />
156
+ );
157
+
158
+ const buttons = screen.getAllByRole('button');
159
+ const activeButtons = buttons.filter(btn =>
160
+ btn.className.includes('preview-mode-group__btn--active')
161
+ );
162
+
163
+ expect(activeButtons).toHaveLength(1);
164
+ });
165
+
166
+ it('applies base class to all buttons', () => {
167
+ renderWithIntl(
168
+ <PreviewModeGroup
169
+ value={PREVIEW_MODES.DESKTOP}
170
+ onChange={mockOnChange}
171
+ />
172
+ );
173
+
174
+ const buttons = screen.getAllByRole('button');
175
+
176
+ buttons.forEach(button => {
177
+ expect(button).toHaveClass('preview-mode-group__btn');
178
+ });
179
+ });
180
+ });
181
+
182
+ describe('User Interactions', () => {
183
+ it('calls onChange with DESKTOP when desktop button is clicked', () => {
184
+ renderWithIntl(
185
+ <PreviewModeGroup
186
+ value={PREVIEW_MODES.MOBILE}
187
+ onChange={mockOnChange}
188
+ />
189
+ );
190
+
191
+ const buttons = screen.getAllByRole('button');
192
+ const desktopButton = buttons[0];
193
+
194
+ fireEvent.click(desktopButton);
195
+
196
+ expect(mockOnChange).toHaveBeenCalledTimes(1);
197
+ expect(mockOnChange).toHaveBeenCalledWith(PREVIEW_MODES.DESKTOP);
198
+ });
199
+
200
+ it('calls onChange with MOBILE when mobile button is clicked', () => {
201
+ renderWithIntl(
202
+ <PreviewModeGroup
203
+ value={PREVIEW_MODES.DESKTOP}
204
+ onChange={mockOnChange}
205
+ />
206
+ );
207
+
208
+ const buttons = screen.getAllByRole('button');
209
+ const mobileButton = buttons[1];
210
+
211
+ fireEvent.click(mobileButton);
212
+
213
+ expect(mockOnChange).toHaveBeenCalledTimes(1);
214
+ expect(mockOnChange).toHaveBeenCalledWith(PREVIEW_MODES.MOBILE);
215
+ });
216
+
217
+ it('allows clicking the already active button', () => {
218
+ renderWithIntl(
219
+ <PreviewModeGroup
220
+ value={PREVIEW_MODES.DESKTOP}
221
+ onChange={mockOnChange}
222
+ />
223
+ );
224
+
225
+ const buttons = screen.getAllByRole('button');
226
+ const desktopButton = buttons[0];
227
+
228
+ fireEvent.click(desktopButton);
229
+
230
+ expect(mockOnChange).toHaveBeenCalledWith(PREVIEW_MODES.DESKTOP);
231
+ });
232
+
233
+ it('handles multiple clicks', () => {
234
+ renderWithIntl(
235
+ <PreviewModeGroup
236
+ value={PREVIEW_MODES.DESKTOP}
237
+ onChange={mockOnChange}
238
+ />
239
+ );
240
+
241
+ const buttons = screen.getAllByRole('button');
242
+ const desktopButton = buttons[0];
243
+ const mobileButton = buttons[1];
244
+
245
+ fireEvent.click(mobileButton);
246
+ fireEvent.click(desktopButton);
247
+ fireEvent.click(mobileButton);
248
+
249
+ expect(mockOnChange).toHaveBeenCalledTimes(3);
250
+ expect(mockOnChange).toHaveBeenNthCalledWith(1, PREVIEW_MODES.MOBILE);
251
+ expect(mockOnChange).toHaveBeenNthCalledWith(2, PREVIEW_MODES.DESKTOP);
252
+ expect(mockOnChange).toHaveBeenNthCalledWith(3, PREVIEW_MODES.MOBILE);
253
+ });
254
+ });
255
+
256
+ describe('Disabled State', () => {
257
+ it('disables both buttons when disabled prop is true', () => {
258
+ renderWithIntl(
259
+ <PreviewModeGroup
260
+ value={PREVIEW_MODES.DESKTOP}
261
+ onChange={mockOnChange}
262
+ disabled={true}
263
+ />
264
+ );
265
+
266
+ const buttons = screen.getAllByRole('button');
267
+
268
+ buttons.forEach(button => {
269
+ expect(button).toBeDisabled();
270
+ });
271
+ });
272
+
273
+ it('does not call onChange when disabled button is clicked', () => {
274
+ renderWithIntl(
275
+ <PreviewModeGroup
276
+ value={PREVIEW_MODES.DESKTOP}
277
+ onChange={mockOnChange}
278
+ disabled={true}
279
+ />
280
+ );
281
+
282
+ const buttons = screen.getAllByRole('button');
283
+ const mobileButton = buttons[1];
284
+
285
+ fireEvent.click(mobileButton);
286
+
287
+ expect(mockOnChange).not.toHaveBeenCalled();
288
+ });
289
+
290
+ it('enables buttons when disabled prop is false', () => {
291
+ renderWithIntl(
292
+ <PreviewModeGroup
293
+ value={PREVIEW_MODES.DESKTOP}
294
+ onChange={mockOnChange}
295
+ disabled={false}
296
+ />
297
+ );
298
+
299
+ const buttons = screen.getAllByRole('button');
300
+
301
+ buttons.forEach(button => {
302
+ expect(button).not.toBeDisabled();
303
+ });
304
+ });
305
+
306
+ it('enables buttons by default when disabled prop is not provided', () => {
307
+ renderWithIntl(
308
+ <PreviewModeGroup
309
+ value={PREVIEW_MODES.DESKTOP}
310
+ onChange={mockOnChange}
311
+ />
312
+ );
313
+
314
+ const buttons = screen.getAllByRole('button');
315
+
316
+ buttons.forEach(button => {
317
+ expect(button).not.toBeDisabled();
318
+ });
319
+ });
320
+
321
+ it('can transition from disabled to enabled', () => {
322
+ const { rerender } = renderWithIntl(
323
+ <PreviewModeGroup
324
+ value={PREVIEW_MODES.DESKTOP}
325
+ onChange={mockOnChange}
326
+ disabled={true}
327
+ />
328
+ );
329
+
330
+ let buttons = screen.getAllByRole('button');
331
+ expect(buttons[0]).toBeDisabled();
332
+
333
+ rerender(
334
+ <IntlProvider locale="en" messages={{}}>
335
+ <PreviewModeGroup
336
+ value={PREVIEW_MODES.DESKTOP}
337
+ onChange={mockOnChange}
338
+ disabled={false}
339
+ />
340
+ </IntlProvider>
341
+ );
342
+
343
+ buttons = screen.getAllByRole('button');
344
+ expect(buttons[0]).not.toBeDisabled();
345
+ });
346
+ });
347
+
348
+ describe('Button Types', () => {
349
+ it('renders buttons with flat type', () => {
350
+ renderWithIntl(
351
+ <PreviewModeGroup
352
+ value={PREVIEW_MODES.DESKTOP}
353
+ onChange={mockOnChange}
354
+ />
355
+ );
356
+
357
+ const buttons = screen.getAllByRole('button');
358
+
359
+ buttons.forEach(button => {
360
+ expect(button).toHaveAttribute('data-type', 'flat');
361
+ });
362
+ });
363
+ });
364
+
365
+ describe('Value Updates', () => {
366
+ it('updates active state when value prop changes', () => {
367
+ const { rerender } = renderWithIntl(
368
+ <PreviewModeGroup
369
+ value={PREVIEW_MODES.DESKTOP}
370
+ onChange={mockOnChange}
371
+ />
372
+ );
373
+
374
+ let buttons = screen.getAllByRole('button');
375
+ expect(buttons[0]).toHaveClass('preview-mode-group__btn--active');
376
+ expect(buttons[1]).not.toHaveClass('preview-mode-group__btn--active');
377
+
378
+ rerender(
379
+ <IntlProvider locale="en" messages={{}}>
380
+ <PreviewModeGroup
381
+ value={PREVIEW_MODES.MOBILE}
382
+ onChange={mockOnChange}
383
+ />
384
+ </IntlProvider>
385
+ );
386
+
387
+ buttons = screen.getAllByRole('button');
388
+ expect(buttons[0]).not.toHaveClass('preview-mode-group__btn--active');
389
+ expect(buttons[1]).toHaveClass('preview-mode-group__btn--active');
390
+ });
391
+
392
+ it('maintains correct state through multiple value changes', () => {
393
+ const { rerender } = renderWithIntl(
394
+ <PreviewModeGroup
395
+ value={PREVIEW_MODES.DESKTOP}
396
+ onChange={mockOnChange}
397
+ />
398
+ );
399
+
400
+ // Change to mobile
401
+ rerender(
402
+ <IntlProvider locale="en" messages={{}}>
403
+ <PreviewModeGroup
404
+ value={PREVIEW_MODES.MOBILE}
405
+ onChange={mockOnChange}
406
+ />
407
+ </IntlProvider>
408
+ );
409
+
410
+ let buttons = screen.getAllByRole('button');
411
+ expect(buttons[1]).toHaveClass('preview-mode-group__btn--active');
412
+
413
+ // Change back to desktop
414
+ rerender(
415
+ <IntlProvider locale="en" messages={{}}>
416
+ <PreviewModeGroup
417
+ value={PREVIEW_MODES.DESKTOP}
418
+ onChange={mockOnChange}
419
+ />
420
+ </IntlProvider>
421
+ );
422
+
423
+ buttons = screen.getAllByRole('button');
424
+ expect(buttons[0]).toHaveClass('preview-mode-group__btn--active');
425
+ });
426
+ });
427
+
428
+ describe('PropTypes and Validation', () => {
429
+ it('accepts valid PREVIEW_MODES values', () => {
430
+ expect(() => {
431
+ renderWithIntl(
432
+ <PreviewModeGroup
433
+ value={PREVIEW_MODES.DESKTOP}
434
+ onChange={mockOnChange}
435
+ />
436
+ );
437
+ }).not.toThrow();
438
+
439
+ expect(() => {
440
+ renderWithIntl(
441
+ <PreviewModeGroup
442
+ value={PREVIEW_MODES.MOBILE}
443
+ onChange={mockOnChange}
444
+ />
445
+ );
446
+ }).not.toThrow();
447
+ });
448
+
449
+ it('accepts onChange function', () => {
450
+ expect(() => {
451
+ renderWithIntl(
452
+ <PreviewModeGroup
453
+ value={PREVIEW_MODES.DESKTOP}
454
+ onChange={jest.fn()}
455
+ />
456
+ );
457
+ }).not.toThrow();
458
+ });
459
+
460
+ it('accepts disabled boolean', () => {
461
+ expect(() => {
462
+ renderWithIntl(
463
+ <PreviewModeGroup
464
+ value={PREVIEW_MODES.DESKTOP}
465
+ onChange={mockOnChange}
466
+ disabled={true}
467
+ />
468
+ );
469
+ }).not.toThrow();
470
+ });
471
+ });
472
+
473
+ describe('Integration with IntlProvider', () => {
474
+ it('renders correctly within IntlProvider', () => {
475
+ const { container } = renderWithIntl(
476
+ <PreviewModeGroup
477
+ value={PREVIEW_MODES.DESKTOP}
478
+ onChange={mockOnChange}
479
+ />
480
+ );
481
+
482
+ expect(container.firstChild).toBeInTheDocument();
483
+ });
484
+
485
+ it('works with different locales', () => {
486
+ const { container: enContainer } = render(
487
+ <IntlProvider locale="en" messages={{}}>
488
+ <PreviewModeGroup
489
+ value={PREVIEW_MODES.DESKTOP}
490
+ onChange={mockOnChange}
491
+ />
492
+ </IntlProvider>
493
+ );
494
+
495
+ expect(enContainer.firstChild).toBeInTheDocument();
496
+
497
+ const { container: jaContainer } = render(
498
+ <IntlProvider locale="ja" messages={{}}>
499
+ <PreviewModeGroup
500
+ value={PREVIEW_MODES.DESKTOP}
501
+ onChange={mockOnChange}
502
+ />
503
+ </IntlProvider>
504
+ );
505
+
506
+ expect(jaContainer.firstChild).toBeInTheDocument();
507
+ });
508
+ });
509
+
510
+ describe('Edge Cases', () => {
511
+ it('handles rapid successive clicks', () => {
512
+ renderWithIntl(
513
+ <PreviewModeGroup
514
+ value={PREVIEW_MODES.DESKTOP}
515
+ onChange={mockOnChange}
516
+ />
517
+ );
518
+
519
+ const buttons = screen.getAllByRole('button');
520
+ const mobileButton = buttons[1];
521
+
522
+ // Deterministic rapid clicks - each click is processed immediately
523
+ for (let i = 0; i < 5; i++) {
524
+ fireEvent.click(mobileButton);
525
+ }
526
+
527
+ // All clicks should be processed synchronously
528
+ expect(mockOnChange).toHaveBeenCalledTimes(5);
529
+ expect(mockOnChange).toHaveBeenCalledWith(PREVIEW_MODES.MOBILE);
530
+ });
531
+
532
+ it('handles onChange being replaced', () => {
533
+ const firstOnChange = jest.fn();
534
+ const secondOnChange = jest.fn();
535
+
536
+ const { rerender } = renderWithIntl(
537
+ <PreviewModeGroup
538
+ value={PREVIEW_MODES.DESKTOP}
539
+ onChange={firstOnChange}
540
+ />
541
+ );
542
+
543
+ const buttons = screen.getAllByRole('button');
544
+ fireEvent.click(buttons[1]);
545
+ expect(firstOnChange).toHaveBeenCalledWith(PREVIEW_MODES.MOBILE);
546
+
547
+ rerender(
548
+ <IntlProvider locale="en" messages={{}}>
549
+ <PreviewModeGroup
550
+ value={PREVIEW_MODES.DESKTOP}
551
+ onChange={secondOnChange}
552
+ />
553
+ </IntlProvider>
554
+ );
555
+
556
+ fireEvent.click(buttons[1]);
557
+ expect(secondOnChange).toHaveBeenCalledWith(PREVIEW_MODES.MOBILE);
558
+ expect(firstOnChange).toHaveBeenCalledTimes(1);
559
+ });
560
+ });
561
+
562
+ describe('Accessibility', () => {
563
+ it('renders buttons with proper role', () => {
564
+ renderWithIntl(
565
+ <PreviewModeGroup
566
+ value={PREVIEW_MODES.DESKTOP}
567
+ onChange={mockOnChange}
568
+ />
569
+ );
570
+
571
+ const buttons = screen.getAllByRole('button');
572
+ expect(buttons).toHaveLength(2);
573
+ });
574
+
575
+ it('disabled buttons are not clickable', () => {
576
+ renderWithIntl(
577
+ <PreviewModeGroup
578
+ value={PREVIEW_MODES.DESKTOP}
579
+ onChange={mockOnChange}
580
+ disabled={true}
581
+ />
582
+ );
583
+
584
+ const buttons = screen.getAllByRole('button');
585
+
586
+ buttons.forEach(button => {
587
+ expect(button).toBeDisabled();
588
+ fireEvent.click(button);
589
+ });
590
+
591
+ expect(mockOnChange).not.toHaveBeenCalled();
592
+ });
593
+
594
+ it('maintains focus management', () => {
595
+ renderWithIntl(
596
+ <PreviewModeGroup
597
+ value={PREVIEW_MODES.DESKTOP}
598
+ onChange={mockOnChange}
599
+ />
600
+ );
601
+
602
+ const buttons = screen.getAllByRole('button');
603
+ const desktopButton = buttons[0];
604
+
605
+ desktopButton.focus();
606
+ expect(document.activeElement).toBe(desktopButton);
607
+ });
608
+
609
+ it('supports keyboard navigation', () => {
610
+ renderWithIntl(
611
+ <PreviewModeGroup
612
+ value={PREVIEW_MODES.DESKTOP}
613
+ onChange={mockOnChange}
614
+ />
615
+ );
616
+
617
+ const buttons = screen.getAllByRole('button');
618
+
619
+ // Tab through buttons
620
+ buttons.forEach(button => {
621
+ button.focus();
622
+ expect(document.activeElement).toBe(button);
623
+ });
624
+ });
625
+ });
626
+
627
+ describe('Performance and Memory', () => {
628
+ it('handles frequent re-renders efficiently', () => {
629
+ const { rerender } = renderWithIntl(
630
+ <PreviewModeGroup
631
+ value={PREVIEW_MODES.DESKTOP}
632
+ onChange={mockOnChange}
633
+ />
634
+ );
635
+
636
+ // Multiple re-renders
637
+ for (let i = 0; i < 20; i++) {
638
+ const value = i % 2 === 0 ? PREVIEW_MODES.DESKTOP : PREVIEW_MODES.MOBILE;
639
+ rerender(
640
+ <IntlProvider locale="en" messages={{}}>
641
+ <PreviewModeGroup
642
+ value={value}
643
+ onChange={mockOnChange}
644
+ />
645
+ </IntlProvider>
646
+ );
647
+ }
648
+
649
+ const buttons = screen.getAllByRole('button');
650
+ expect(buttons).toHaveLength(2);
651
+ });
652
+
653
+ it('maintains consistent button references', () => {
654
+ const { rerender } = renderWithIntl(
655
+ <PreviewModeGroup
656
+ value={PREVIEW_MODES.DESKTOP}
657
+ onChange={mockOnChange}
658
+ />
659
+ );
660
+
661
+ const initialButtons = screen.getAllByRole('button');
662
+
663
+ rerender(
664
+ <IntlProvider locale="en" messages={{}}>
665
+ <PreviewModeGroup
666
+ value={PREVIEW_MODES.MOBILE}
667
+ onChange={mockOnChange}
668
+ />
669
+ </IntlProvider>
670
+ );
671
+
672
+ const updatedButtons = screen.getAllByRole('button');
673
+ expect(updatedButtons).toHaveLength(initialButtons.length);
674
+ });
675
+
676
+ it('handles rapid prop changes without memory leaks', () => {
677
+ const { rerender } = renderWithIntl(
678
+ <PreviewModeGroup
679
+ value={PREVIEW_MODES.DESKTOP}
680
+ onChange={mockOnChange}
681
+ />
682
+ );
683
+
684
+ // Rapid prop changes
685
+ for (let i = 0; i < 50; i++) {
686
+ rerender(
687
+ <IntlProvider locale="en" messages={{}}>
688
+ <PreviewModeGroup
689
+ value={i % 2 === 0 ? PREVIEW_MODES.DESKTOP : PREVIEW_MODES.MOBILE}
690
+ onChange={mockOnChange}
691
+ disabled={i % 3 === 0}
692
+ />
693
+ </IntlProvider>
694
+ );
695
+ }
696
+
697
+ expect(screen.getAllByRole('button')).toHaveLength(2);
698
+ });
699
+ });
700
+
701
+ describe('Component Lifecycle', () => {
702
+ it('initializes with correct state', () => {
703
+ renderWithIntl(
704
+ <PreviewModeGroup
705
+ value={PREVIEW_MODES.DESKTOP}
706
+ onChange={mockOnChange}
707
+ />
708
+ );
709
+
710
+ const buttons = screen.getAllByRole('button');
711
+ expect(buttons[0]).toHaveClass('preview-mode-group__btn--active');
712
+ expect(buttons[1]).not.toHaveClass('preview-mode-group__btn--active');
713
+ });
714
+
715
+ it('cleans up properly on unmount', () => {
716
+ const { unmount } = renderWithIntl(
717
+ <PreviewModeGroup
718
+ value={PREVIEW_MODES.DESKTOP}
719
+ onChange={mockOnChange}
720
+ />
721
+ );
722
+
723
+ expect(() => unmount()).not.toThrow();
724
+ });
725
+
726
+ it('handles component updates correctly', () => {
727
+ const { rerender } = renderWithIntl(
728
+ <PreviewModeGroup
729
+ value={PREVIEW_MODES.DESKTOP}
730
+ onChange={mockOnChange}
731
+ disabled={false}
732
+ />
733
+ );
734
+
735
+ // Update multiple props
736
+ rerender(
737
+ <IntlProvider locale="en" messages={{}}>
738
+ <PreviewModeGroup
739
+ value={PREVIEW_MODES.MOBILE}
740
+ onChange={jest.fn()}
741
+ disabled={true}
742
+ />
743
+ </IntlProvider>
744
+ );
745
+
746
+ const buttons = screen.getAllByRole('button');
747
+ expect(buttons[1]).toHaveClass('preview-mode-group__btn--active');
748
+ expect(buttons[0]).toBeDisabled();
749
+ expect(buttons[1]).toBeDisabled();
750
+ });
751
+ });
752
+
753
+ describe('Error Handling and Edge Cases', () => {
754
+ it('handles null onChange gracefully', () => {
755
+ // This should not crash the component
756
+ expect(() => {
757
+ renderWithIntl(
758
+ <PreviewModeGroup
759
+ value={PREVIEW_MODES.DESKTOP}
760
+ onChange={null}
761
+ />
762
+ );
763
+ }).not.toThrow();
764
+ });
765
+
766
+ it('handles undefined value gracefully', () => {
767
+ // Component should still render even with undefined value
768
+ expect(() => {
769
+ renderWithIntl(
770
+ <PreviewModeGroup
771
+ value={undefined}
772
+ onChange={mockOnChange}
773
+ />
774
+ );
775
+ }).not.toThrow();
776
+ });
777
+
778
+ it('maintains functionality with concurrent clicks', () => {
779
+ // Use fake timers for deterministic behavior
780
+ jest.useFakeTimers();
781
+
782
+ renderWithIntl(
783
+ <PreviewModeGroup
784
+ value={PREVIEW_MODES.DESKTOP}
785
+ onChange={mockOnChange}
786
+ />
787
+ );
788
+
789
+ const buttons = screen.getAllByRole('button');
790
+
791
+ // Simulate deterministic "concurrent" clicks using fake timers
792
+ for (let i = 0; i < 10; i++) {
793
+ setTimeout(() => {
794
+ fireEvent.click(buttons[i % 2]);
795
+ }, i * 10); // Deterministic delays: 0ms, 10ms, 20ms, etc.
796
+ }
797
+
798
+ // Advance timers to execute all scheduled clicks
799
+ jest.advanceTimersByTime(100); // Advance past all scheduled timeouts
800
+
801
+ expect(mockOnChange).toHaveBeenCalledTimes(10);
802
+
803
+ // Restore real timers
804
+ jest.useRealTimers();
805
+ });
806
+
807
+ it('handles extreme prop combinations', () => {
808
+ expect(() => {
809
+ renderWithIntl(
810
+ <PreviewModeGroup
811
+ value={PREVIEW_MODES.MOBILE}
812
+ onChange={mockOnChange}
813
+ disabled={true}
814
+ />
815
+ );
816
+ }).not.toThrow();
817
+
818
+ const buttons = screen.getAllByRole('button');
819
+ expect(buttons[1]).toHaveClass('preview-mode-group__btn--active');
820
+ expect(buttons[1]).toBeDisabled();
821
+ });
822
+ });
823
+
824
+ describe('Constants Integration', () => {
825
+ it('uses PREVIEW_MODES constants correctly', () => {
826
+ renderWithIntl(
827
+ <PreviewModeGroup
828
+ value={PREVIEW_MODES.DESKTOP}
829
+ onChange={mockOnChange}
830
+ />
831
+ );
832
+
833
+ const buttons = screen.getAllByRole('button');
834
+ fireEvent.click(buttons[1]);
835
+
836
+ expect(mockOnChange).toHaveBeenCalledWith(PREVIEW_MODES.MOBILE);
837
+ });
838
+
839
+ it('handles all valid PREVIEW_MODES values', () => {
840
+ const modes = Object.values(PREVIEW_MODES);
841
+
842
+ modes.forEach(mode => {
843
+ const { unmount } = renderWithIntl(
844
+ <PreviewModeGroup
845
+ value={mode}
846
+ onChange={mockOnChange}
847
+ />
848
+ );
849
+
850
+ expect(screen.getAllByRole('button')).toHaveLength(2);
851
+ unmount();
852
+ });
853
+ });
854
+
855
+ it('maintains consistency with PREVIEW_MODES enum', () => {
856
+ expect(PREVIEW_MODES.DESKTOP).toBeDefined();
857
+ expect(PREVIEW_MODES.MOBILE).toBeDefined();
858
+ expect(typeof PREVIEW_MODES.DESKTOP).toBe('string');
859
+ expect(typeof PREVIEW_MODES.MOBILE).toBe('string');
860
+ });
861
+ });
862
+
863
+ describe('Cap UI Integration', () => {
864
+ it('passes correct props to CapButton components', () => {
865
+ renderWithIntl(
866
+ <PreviewModeGroup
867
+ value={PREVIEW_MODES.DESKTOP}
868
+ onChange={mockOnChange}
869
+ disabled={true}
870
+ />
871
+ );
872
+
873
+ const buttons = screen.getAllByRole('button');
874
+
875
+ buttons.forEach(button => {
876
+ expect(button).toHaveAttribute('data-type', 'flat');
877
+ expect(button).toBeDisabled();
878
+ });
879
+ });
880
+
881
+ it('passes correct props to CapIcon components', () => {
882
+ renderWithIntl(
883
+ <PreviewModeGroup
884
+ value={PREVIEW_MODES.DESKTOP}
885
+ onChange={mockOnChange}
886
+ />
887
+ );
888
+
889
+ const desktopIcon = screen.getByTestId('icon-desktop');
890
+ const mobileIcon = screen.getByTestId('icon-mobile');
891
+
892
+ expect(desktopIcon).toHaveAttribute('data-size', 'm');
893
+ expect(mobileIcon).toHaveAttribute('data-size', 'm');
894
+ expect(desktopIcon).toHaveTextContent('desktop');
895
+ expect(mobileIcon).toHaveTextContent('mobile');
896
+ });
897
+
898
+ it('maintains Cap UI component structure', () => {
899
+ const { container } = renderWithIntl(
900
+ <PreviewModeGroup
901
+ value={PREVIEW_MODES.DESKTOP}
902
+ onChange={mockOnChange}
903
+ />
904
+ );
905
+
906
+ // Should have the preview-mode-group container
907
+ expect(container.querySelector('.preview-mode-group')).toBeInTheDocument();
908
+
909
+ // Should have buttons with Cap UI structure
910
+ const buttons = container.querySelectorAll('button');
911
+ expect(buttons).toHaveLength(2);
912
+ });
913
+ });
914
+
915
+ describe('Styling and Layout', () => {
916
+ it('applies correct container styles', () => {
917
+ const { container } = renderWithIntl(
918
+ <PreviewModeGroup
919
+ value={PREVIEW_MODES.DESKTOP}
920
+ onChange={mockOnChange}
921
+ />
922
+ );
923
+
924
+ const previewGroup = container.querySelector('.preview-mode-group');
925
+ expect(previewGroup).toHaveClass('preview-mode-group');
926
+
927
+ // Verify the container has the expected structure for CSS styling
928
+ expect(previewGroup.tagName.toLowerCase()).toBe('div');
929
+ });
930
+
931
+ it('applies correct button classes based on state', () => {
932
+ const { rerender } = renderWithIntl(
933
+ <PreviewModeGroup
934
+ value={PREVIEW_MODES.DESKTOP}
935
+ onChange={mockOnChange}
936
+ />
937
+ );
938
+
939
+ let buttons = screen.getAllByRole('button');
940
+ expect(buttons[0]).toHaveClass('preview-mode-group__btn', 'preview-mode-group__btn--active');
941
+ expect(buttons[1]).toHaveClass('preview-mode-group__btn');
942
+ expect(buttons[1]).not.toHaveClass('preview-mode-group__btn--active');
943
+
944
+ rerender(
945
+ <IntlProvider locale="en" messages={{}}>
946
+ <PreviewModeGroup
947
+ value={PREVIEW_MODES.MOBILE}
948
+ onChange={mockOnChange}
949
+ />
950
+ </IntlProvider>
951
+ );
952
+
953
+ buttons = screen.getAllByRole('button');
954
+ expect(buttons[0]).toHaveClass('preview-mode-group__btn');
955
+ expect(buttons[0]).not.toHaveClass('preview-mode-group__btn--active');
956
+ expect(buttons[1]).toHaveClass('preview-mode-group__btn', 'preview-mode-group__btn--active');
957
+ });
958
+
959
+ it('maintains consistent layout structure', () => {
960
+ const { container } = renderWithIntl(
961
+ <PreviewModeGroup
962
+ value={PREVIEW_MODES.DESKTOP}
963
+ onChange={mockOnChange}
964
+ />
965
+ );
966
+
967
+ const previewGroup = container.querySelector('.preview-mode-group');
968
+ const buttons = previewGroup.querySelectorAll('button');
969
+
970
+ expect(buttons).toHaveLength(2);
971
+ expect(previewGroup.children).toHaveLength(2);
972
+ });
973
+ });
974
+
975
+ describe('Accessibility and ARIA Attributes', () => {
976
+ it('sets correct aria-label attributes for desktop button', () => {
977
+ renderWithIntl(
978
+ <PreviewModeGroup
979
+ value={PREVIEW_MODES.DESKTOP}
980
+ onChange={mockOnChange}
981
+ />
982
+ );
983
+
984
+ const buttons = screen.getAllByRole('button');
985
+ const desktopButton = buttons[0];
986
+
987
+ expect(desktopButton).toHaveAttribute('aria-label');
988
+ expect(desktopButton).toHaveAttribute('aria-pressed', 'true');
989
+ });
990
+
991
+ it('sets correct aria-label attributes for mobile button', () => {
992
+ renderWithIntl(
993
+ <PreviewModeGroup
994
+ value={PREVIEW_MODES.MOBILE}
995
+ onChange={mockOnChange}
996
+ />
997
+ );
998
+
999
+ const buttons = screen.getAllByRole('button');
1000
+ const mobileButton = buttons[1];
1001
+
1002
+ expect(mobileButton).toHaveAttribute('aria-label');
1003
+ expect(mobileButton).toHaveAttribute('aria-pressed', 'true');
1004
+ });
1005
+
1006
+ it('updates aria-pressed attributes correctly when value changes', () => {
1007
+ const { rerender } = renderWithIntl(
1008
+ <PreviewModeGroup
1009
+ value={PREVIEW_MODES.DESKTOP}
1010
+ onChange={mockOnChange}
1011
+ />
1012
+ );
1013
+
1014
+ let buttons = screen.getAllByRole('button');
1015
+ expect(buttons[0]).toHaveAttribute('aria-pressed', 'true');
1016
+ expect(buttons[1]).toHaveAttribute('aria-pressed', 'false');
1017
+
1018
+ rerender(
1019
+ <IntlProvider locale="en" messages={{}}>
1020
+ <PreviewModeGroup
1021
+ value={PREVIEW_MODES.MOBILE}
1022
+ onChange={mockOnChange}
1023
+ />
1024
+ </IntlProvider>
1025
+ );
1026
+
1027
+ buttons = screen.getAllByRole('button');
1028
+ expect(buttons[0]).toHaveAttribute('aria-pressed', 'false');
1029
+ expect(buttons[1]).toHaveAttribute('aria-pressed', 'true');
1030
+ });
1031
+
1032
+ it('maintains aria attributes when disabled', () => {
1033
+ renderWithIntl(
1034
+ <PreviewModeGroup
1035
+ value={PREVIEW_MODES.DESKTOP}
1036
+ onChange={mockOnChange}
1037
+ disabled={true}
1038
+ />
1039
+ );
1040
+
1041
+ const buttons = screen.getAllByRole('button');
1042
+
1043
+ buttons.forEach(button => {
1044
+ expect(button).toHaveAttribute('aria-label');
1045
+ expect(button).toHaveAttribute('aria-pressed');
1046
+ });
1047
+ });
1048
+ });
1049
+
1050
+ describe('Internationalization (i18n)', () => {
1051
+ it('formats desktop message using intl', () => {
1052
+ const customMessages = {
1053
+ 'app.components.HtmlEditor.desktop': 'Desktop View',
1054
+ 'app.components.HtmlEditor.mobile': 'Mobile View'
1055
+ };
1056
+
1057
+ render(
1058
+ <IntlProvider locale="en" messages={customMessages}>
1059
+ <PreviewModeGroup
1060
+ value={PREVIEW_MODES.DESKTOP}
1061
+ onChange={mockOnChange}
1062
+ />
1063
+ </IntlProvider>
1064
+ );
1065
+
1066
+ const buttons = screen.getAllByRole('button');
1067
+ // The aria-label should be formatted by intl
1068
+ expect(buttons[0]).toHaveAttribute('aria-label');
1069
+ });
1070
+
1071
+ it('formats mobile message using intl', () => {
1072
+ const customMessages = {
1073
+ 'app.components.HtmlEditor.desktop': 'Desktop View',
1074
+ 'app.components.HtmlEditor.mobile': 'Mobile View'
1075
+ };
1076
+
1077
+ render(
1078
+ <IntlProvider locale="en" messages={customMessages}>
1079
+ <PreviewModeGroup
1080
+ value={PREVIEW_MODES.MOBILE}
1081
+ onChange={mockOnChange}
1082
+ />
1083
+ </IntlProvider>
1084
+ );
1085
+
1086
+ const buttons = screen.getAllByRole('button');
1087
+ // The aria-label should be formatted by intl
1088
+ expect(buttons[1]).toHaveAttribute('aria-label');
1089
+ });
1090
+
1091
+ it('handles missing message keys gracefully', () => {
1092
+ render(
1093
+ <IntlProvider locale="en" messages={{}}>
1094
+ <PreviewModeGroup
1095
+ value={PREVIEW_MODES.DESKTOP}
1096
+ onChange={mockOnChange}
1097
+ />
1098
+ </IntlProvider>
1099
+ );
1100
+
1101
+ // Should still render without crashing even with missing messages
1102
+ const buttons = screen.getAllByRole('button');
1103
+ expect(buttons).toHaveLength(2);
1104
+ });
1105
+
1106
+ it('works with different locale formats', () => {
1107
+ const frenchMessages = {
1108
+ 'app.components.HtmlEditor.desktop': 'Vue Bureau',
1109
+ 'app.components.HtmlEditor.mobile': 'Vue Mobile'
1110
+ };
1111
+
1112
+ render(
1113
+ <IntlProvider locale="fr" messages={frenchMessages}>
1114
+ <PreviewModeGroup
1115
+ value={PREVIEW_MODES.DESKTOP}
1116
+ onChange={mockOnChange}
1117
+ />
1118
+ </IntlProvider>
1119
+ );
1120
+
1121
+ const buttons = screen.getAllByRole('button');
1122
+ expect(buttons).toHaveLength(2);
1123
+ expect(buttons[0]).toHaveAttribute('aria-label');
1124
+ });
1125
+ });
1126
+
1127
+ describe('Component Internal Functions', () => {
1128
+ it('handleChange function exists and can be called', () => {
1129
+ // Test the internal handleChange function by creating a scenario where it might be used
1130
+ const TestComponent = () => {
1131
+ const [value, setValue] = React.useState(PREVIEW_MODES.DESKTOP);
1132
+
1133
+ const handleChange = (newValue) => {
1134
+ setValue(newValue);
1135
+ };
1136
+
1137
+ return (
1138
+ <PreviewModeGroup
1139
+ value={value}
1140
+ onChange={handleChange}
1141
+ />
1142
+ );
1143
+ };
1144
+
1145
+ render(
1146
+ <IntlProvider locale="en" messages={{}}>
1147
+ <TestComponent />
1148
+ </IntlProvider>
1149
+ );
1150
+
1151
+ const buttons = screen.getAllByRole('button');
1152
+ fireEvent.click(buttons[1]);
1153
+
1154
+ // Should update the state through the handleChange function
1155
+ expect(buttons[1]).toHaveClass('preview-mode-group__btn--active');
1156
+ });
1157
+
1158
+ it('handles button click to change mode from desktop to mobile', () => {
1159
+ // Test button click interaction to change preview mode
1160
+ renderWithIntl(
1161
+ <PreviewModeGroup
1162
+ value={PREVIEW_MODES.DESKTOP}
1163
+ onChange={mockOnChange}
1164
+ />
1165
+ );
1166
+
1167
+ const buttons = screen.getAllByRole('button');
1168
+ const mobileButton = buttons[1]; // Second button is mobile
1169
+
1170
+ fireEvent.click(mobileButton);
1171
+
1172
+ expect(mockOnChange).toHaveBeenCalledWith(PREVIEW_MODES.MOBILE);
1173
+ });
1174
+
1175
+ it('changes preview mode through user interaction with mobile button', () => {
1176
+ // Test real component interaction - user clicking mobile button
1177
+ renderWithIntl(
1178
+ <PreviewModeGroup
1179
+ value={PREVIEW_MODES.DESKTOP}
1180
+ onChange={mockOnChange}
1181
+ />
1182
+ );
1183
+
1184
+ // Find the mobile button (second button in the group)
1185
+ const buttons = screen.getAllByRole('button');
1186
+ const mobileButton = buttons[1];
1187
+
1188
+ // Verify initial state - desktop should be active
1189
+ expect(buttons[0]).toHaveClass('preview-mode-group__btn--active');
1190
+ expect(mobileButton).not.toHaveClass('preview-mode-group__btn--active');
1191
+
1192
+ // Simulate user clicking the mobile button
1193
+ fireEvent.click(mobileButton);
1194
+
1195
+ // Assert that onChange was called with the correct mode
1196
+ expect(mockOnChange).toHaveBeenCalledWith(PREVIEW_MODES.MOBILE);
1197
+ expect(mockOnChange).toHaveBeenCalledTimes(1);
1198
+ });
1199
+
1200
+ it('mock CapButton forwards value prop and other props to DOM button elements', () => {
1201
+ // Test that our mock CapButton properly forwards props
1202
+ const MockCapButton = jest.requireMock('@capillarytech/cap-ui-library/CapButton');
1203
+
1204
+ const TestComponent = () => (
1205
+ <MockCapButton
1206
+ value={PREVIEW_MODES.DESKTOP}
1207
+ onClick={mockOnChange}
1208
+ data-testid="test-button"
1209
+ custom-attribute="test-value"
1210
+ >
1211
+ Test Button
1212
+ </MockCapButton>
1213
+ );
1214
+
1215
+ render(<TestComponent />);
1216
+
1217
+ const button = screen.getByTestId('test-button');
1218
+
1219
+ // Check that value and custom attributes are forwarded via ...rest
1220
+ expect(button).toHaveAttribute('value', PREVIEW_MODES.DESKTOP);
1221
+ expect(button).toHaveAttribute('custom-attribute', 'test-value');
1222
+ });
1223
+
1224
+ it('enables event.target.value access in event handlers', () => {
1225
+ const handleChangeWithEvent = jest.fn((e) => {
1226
+ // This test verifies that e.target.value is available
1227
+ expect(e.target.value).toBeDefined();
1228
+ mockOnChange(e.target.value);
1229
+ });
1230
+
1231
+ const TestComponent = () => {
1232
+ return (
1233
+ <div>
1234
+ <button
1235
+ value={PREVIEW_MODES.MOBILE}
1236
+ onClick={handleChangeWithEvent}
1237
+ data-testid="test-button"
1238
+ >
1239
+ Test Button
1240
+ </button>
1241
+ </div>
1242
+ );
1243
+ };
1244
+
1245
+ renderWithIntl(<TestComponent />);
1246
+
1247
+ const testButton = screen.getByTestId('test-button');
1248
+ fireEvent.click(testButton);
1249
+
1250
+ expect(handleChangeWithEvent).toHaveBeenCalled();
1251
+ expect(mockOnChange).toHaveBeenCalledWith(PREVIEW_MODES.MOBILE);
1252
+ });
1253
+ });
1254
+
1255
+ describe('Branch Coverage Tests', () => {
1256
+ it('covers all conditional branches for desktop active state', () => {
1257
+ const { rerender } = renderWithIntl(
1258
+ <PreviewModeGroup
1259
+ value={PREVIEW_MODES.DESKTOP}
1260
+ onChange={mockOnChange}
1261
+ />
1262
+ );
1263
+
1264
+ let buttons = screen.getAllByRole('button');
1265
+ // Desktop button should have active class
1266
+ expect(buttons[0]).toHaveClass('preview-mode-group__btn--active');
1267
+ // Mobile button should not have active class
1268
+ expect(buttons[1]).not.toHaveClass('preview-mode-group__btn--active');
1269
+
1270
+ // Test the opposite condition
1271
+ rerender(
1272
+ <IntlProvider locale="en" messages={{}}>
1273
+ <PreviewModeGroup
1274
+ value={PREVIEW_MODES.MOBILE}
1275
+ onChange={mockOnChange}
1276
+ />
1277
+ </IntlProvider>
1278
+ );
1279
+
1280
+ buttons = screen.getAllByRole('button');
1281
+ // Desktop button should not have active class
1282
+ expect(buttons[0]).not.toHaveClass('preview-mode-group__btn--active');
1283
+ // Mobile button should have active class
1284
+ expect(buttons[1]).toHaveClass('preview-mode-group__btn--active');
1285
+ });
1286
+
1287
+ it('covers all conditional branches for mobile active state', () => {
1288
+ const { rerender } = renderWithIntl(
1289
+ <PreviewModeGroup
1290
+ value={PREVIEW_MODES.MOBILE}
1291
+ onChange={mockOnChange}
1292
+ />
1293
+ );
1294
+
1295
+ let buttons = screen.getAllByRole('button');
1296
+ expect(buttons[1]).toHaveClass('preview-mode-group__btn--active');
1297
+ expect(buttons[0]).not.toHaveClass('preview-mode-group__btn--active');
1298
+
1299
+ // Test aria-pressed attributes
1300
+ expect(buttons[1]).toHaveAttribute('aria-pressed', 'true');
1301
+ expect(buttons[0]).toHaveAttribute('aria-pressed', 'false');
1302
+
1303
+ // Switch to desktop to test the other branch
1304
+ rerender(
1305
+ <IntlProvider locale="en" messages={{}}>
1306
+ <PreviewModeGroup
1307
+ value={PREVIEW_MODES.DESKTOP}
1308
+ onChange={mockOnChange}
1309
+ />
1310
+ </IntlProvider>
1311
+ );
1312
+
1313
+ buttons = screen.getAllByRole('button');
1314
+ expect(buttons[0]).toHaveAttribute('aria-pressed', 'true');
1315
+ expect(buttons[1]).toHaveAttribute('aria-pressed', 'false');
1316
+ });
1317
+
1318
+ it('covers disabled prop branches', () => {
1319
+ const { rerender } = renderWithIntl(
1320
+ <PreviewModeGroup
1321
+ value={PREVIEW_MODES.DESKTOP}
1322
+ onChange={mockOnChange}
1323
+ disabled={true}
1324
+ />
1325
+ );
1326
+
1327
+ let buttons = screen.getAllByRole('button');
1328
+ buttons.forEach(button => {
1329
+ expect(button).toBeDisabled();
1330
+ });
1331
+
1332
+ // Test enabled state
1333
+ rerender(
1334
+ <IntlProvider locale="en" messages={{}}>
1335
+ <PreviewModeGroup
1336
+ value={PREVIEW_MODES.DESKTOP}
1337
+ onChange={mockOnChange}
1338
+ disabled={false}
1339
+ />
1340
+ </IntlProvider>
1341
+ );
1342
+
1343
+ buttons = screen.getAllByRole('button');
1344
+ buttons.forEach(button => {
1345
+ expect(button).not.toBeDisabled();
1346
+ });
1347
+ });
1348
+
1349
+ it('covers className conditional branches thoroughly', () => {
1350
+ // Test all combinations of active states and class assignments
1351
+ const testCases = [
1352
+ { value: PREVIEW_MODES.DESKTOP, expectedActiveIndex: 0 },
1353
+ { value: PREVIEW_MODES.MOBILE, expectedActiveIndex: 1 }
1354
+ ];
1355
+
1356
+ testCases.forEach(({ value, expectedActiveIndex }) => {
1357
+ const { unmount } = renderWithIntl(
1358
+ <PreviewModeGroup
1359
+ value={value}
1360
+ onChange={mockOnChange}
1361
+ />
1362
+ );
1363
+
1364
+ const buttons = screen.getAllByRole('button');
1365
+
1366
+ buttons.forEach((button, index) => {
1367
+ expect(button).toHaveClass('preview-mode-group__btn');
1368
+
1369
+ if (index === expectedActiveIndex) {
1370
+ expect(button).toHaveClass('preview-mode-group__btn--active');
1371
+ } else {
1372
+ expect(button).not.toHaveClass('preview-mode-group__btn--active');
1373
+ }
1374
+ });
1375
+
1376
+ unmount();
1377
+ });
1378
+ });
1379
+ });
1380
+
1381
+ describe('Advanced Edge Cases', () => {
1382
+ it('handles component re-mounting', () => {
1383
+ const { unmount } = renderWithIntl(
1384
+ <PreviewModeGroup
1385
+ value={PREVIEW_MODES.DESKTOP}
1386
+ onChange={mockOnChange}
1387
+ />
1388
+ );
1389
+
1390
+ unmount();
1391
+
1392
+ // Re-mount with new render call
1393
+ renderWithIntl(
1394
+ <PreviewModeGroup
1395
+ value={PREVIEW_MODES.MOBILE}
1396
+ onChange={mockOnChange}
1397
+ />
1398
+ );
1399
+
1400
+ const buttons = screen.getAllByRole('button');
1401
+ expect(buttons[1]).toHaveClass('preview-mode-group__btn--active');
1402
+ });
1403
+
1404
+ it('handles invalid PREVIEW_MODES values gracefully', () => {
1405
+ // Test with an invalid value
1406
+ expect(() => {
1407
+ renderWithIntl(
1408
+ <PreviewModeGroup
1409
+ value="invalid-mode"
1410
+ onChange={mockOnChange}
1411
+ />
1412
+ );
1413
+ }).not.toThrow();
1414
+
1415
+ const buttons = screen.getAllByRole('button');
1416
+ expect(buttons).toHaveLength(2);
1417
+ });
1418
+
1419
+ it('maintains functionality with complex prop combinations', () => {
1420
+ const complexProps = {
1421
+ value: PREVIEW_MODES.DESKTOP,
1422
+ onChange: mockOnChange,
1423
+ disabled: false
1424
+ };
1425
+
1426
+ renderWithIntl(<PreviewModeGroup {...complexProps} />);
1427
+
1428
+ const buttons = screen.getAllByRole('button');
1429
+ fireEvent.click(buttons[1]);
1430
+
1431
+ expect(mockOnChange).toHaveBeenCalledWith(PREVIEW_MODES.MOBILE);
1432
+ });
1433
+
1434
+ it('handles onChange function replacement during lifecycle', () => {
1435
+ const firstHandler = jest.fn();
1436
+ const secondHandler = jest.fn();
1437
+
1438
+ const { rerender } = renderWithIntl(
1439
+ <PreviewModeGroup
1440
+ value={PREVIEW_MODES.DESKTOP}
1441
+ onChange={firstHandler}
1442
+ />
1443
+ );
1444
+
1445
+ const buttons = screen.getAllByRole('button');
1446
+ fireEvent.click(buttons[1]);
1447
+ expect(firstHandler).toHaveBeenCalledWith(PREVIEW_MODES.MOBILE);
1448
+
1449
+ rerender(
1450
+ <IntlProvider locale="en" messages={{}}>
1451
+ <PreviewModeGroup
1452
+ value={PREVIEW_MODES.DESKTOP}
1453
+ onChange={secondHandler}
1454
+ />
1455
+ </IntlProvider>
1456
+ );
1457
+
1458
+ fireEvent.click(buttons[1]);
1459
+ expect(secondHandler).toHaveBeenCalledWith(PREVIEW_MODES.MOBILE);
1460
+ expect(firstHandler).toHaveBeenCalledTimes(1);
1461
+ });
1462
+ });
1463
+
1464
+ describe('Component State Management', () => {
1465
+ it('maintains consistent state across re-renders', () => {
1466
+ const { rerender } = renderWithIntl(
1467
+ <PreviewModeGroup
1468
+ value={PREVIEW_MODES.DESKTOP}
1469
+ onChange={mockOnChange}
1470
+ />
1471
+ );
1472
+
1473
+ // Multiple re-renders with same props
1474
+ for (let i = 0; i < 5; i++) {
1475
+ rerender(
1476
+ <IntlProvider locale="en" messages={{}}>
1477
+ <PreviewModeGroup
1478
+ value={PREVIEW_MODES.DESKTOP}
1479
+ onChange={mockOnChange}
1480
+ />
1481
+ </IntlProvider>
1482
+ );
1483
+ }
1484
+
1485
+ const buttons = screen.getAllByRole('button');
1486
+ expect(buttons[0]).toHaveClass('preview-mode-group__btn--active');
1487
+ });
1488
+
1489
+ it('handles state transitions correctly', () => {
1490
+ const states = [
1491
+ PREVIEW_MODES.DESKTOP,
1492
+ PREVIEW_MODES.MOBILE,
1493
+ PREVIEW_MODES.DESKTOP,
1494
+ PREVIEW_MODES.MOBILE
1495
+ ];
1496
+
1497
+ const { rerender } = renderWithIntl(
1498
+ <PreviewModeGroup
1499
+ value={states[0]}
1500
+ onChange={mockOnChange}
1501
+ />
1502
+ );
1503
+
1504
+ states.forEach((state, index) => {
1505
+ rerender(
1506
+ <IntlProvider locale="en" messages={{}}>
1507
+ <PreviewModeGroup
1508
+ value={state}
1509
+ onChange={mockOnChange}
1510
+ />
1511
+ </IntlProvider>
1512
+ );
1513
+
1514
+ const buttons = screen.getAllByRole('button');
1515
+ if (state === PREVIEW_MODES.DESKTOP) {
1516
+ expect(buttons[0]).toHaveClass('preview-mode-group__btn--active');
1517
+ } else {
1518
+ expect(buttons[1]).toHaveClass('preview-mode-group__btn--active');
1519
+ }
1520
+ });
1521
+ });
1522
+ });
1523
+
1524
+ describe('Real-world Usage Scenarios', () => {
1525
+ it('supports typical preview mode switching workflow', () => {
1526
+ const { rerender } = renderWithIntl(
1527
+ <PreviewModeGroup
1528
+ value={PREVIEW_MODES.DESKTOP}
1529
+ onChange={mockOnChange}
1530
+ />
1531
+ );
1532
+
1533
+ const buttons = screen.getAllByRole('button');
1534
+
1535
+ // User starts with desktop, switches to mobile
1536
+ expect(buttons[0]).toHaveClass('preview-mode-group__btn--active');
1537
+
1538
+ fireEvent.click(buttons[1]);
1539
+ expect(mockOnChange).toHaveBeenCalledWith(PREVIEW_MODES.MOBILE);
1540
+
1541
+ // Simulate parent updating value to mobile
1542
+ rerender(
1543
+ <IntlProvider locale="en" messages={{}}>
1544
+ <PreviewModeGroup
1545
+ value={PREVIEW_MODES.MOBILE}
1546
+ onChange={mockOnChange}
1547
+ />
1548
+ </IntlProvider>
1549
+ );
1550
+
1551
+ const updatedButtons = screen.getAllByRole('button');
1552
+ expect(updatedButtons[1]).toHaveClass('preview-mode-group__btn--active');
1553
+ expect(updatedButtons[0]).not.toHaveClass('preview-mode-group__btn--active');
1554
+ });
1555
+
1556
+ it('supports editor toolbar integration', () => {
1557
+ // Simulate being used within a larger toolbar
1558
+ const { container } = render(
1559
+ <IntlProvider locale="en" messages={{}}>
1560
+ <div className="editor-toolbar">
1561
+ <div className="toolbar-section">
1562
+ <PreviewModeGroup
1563
+ value={PREVIEW_MODES.DESKTOP}
1564
+ onChange={mockOnChange}
1565
+ />
1566
+ </div>
1567
+ </div>
1568
+ </IntlProvider>
1569
+ );
1570
+
1571
+ expect(container.querySelector('.editor-toolbar')).toBeInTheDocument();
1572
+ expect(container.querySelector('.preview-mode-group')).toBeInTheDocument();
1573
+ });
1574
+
1575
+ it('supports responsive design scenarios', () => {
1576
+ renderWithIntl(
1577
+ <PreviewModeGroup
1578
+ value={PREVIEW_MODES.DESKTOP}
1579
+ onChange={mockOnChange}
1580
+ />
1581
+ );
1582
+
1583
+ // Component should maintain functionality regardless of container size
1584
+ const buttons = screen.getAllByRole('button');
1585
+ expect(buttons).toHaveLength(2);
1586
+
1587
+ // Should work in different viewport scenarios
1588
+ fireEvent.click(buttons[1]);
1589
+ expect(mockOnChange).toHaveBeenCalledWith(PREVIEW_MODES.MOBILE);
1590
+ });
1591
+ });
1592
+ });
1593
+
1594
+