@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,580 @@
1
+ /**
2
+ * useLayoutState Hook Tests
3
+ *
4
+ * Tests for the useLayoutState custom hook that manages editor layout state.
5
+ */
6
+
7
+ import React from 'react';
8
+ import { render, screen, fireEvent, act } from '@testing-library/react';
9
+ import '@testing-library/jest-dom';
10
+ import { useLayoutState } from '../useLayoutState';
11
+ import { PREVIEW_MODES, LAYOUT } from '../../constants';
12
+
13
+ // Test wrapper component
14
+ const TestComponent = ({ initialLayout, onStateChange }) => {
15
+ const layoutState = useLayoutState(initialLayout);
16
+
17
+ React.useEffect(() => {
18
+ if (onStateChange) {
19
+ onStateChange(layoutState);
20
+ }
21
+ });
22
+
23
+ return (
24
+ <div>
25
+ <div data-testid="split-size">{layoutState.splitSize}</div>
26
+ <div data-testid="view-mode">{layoutState.viewMode}</div>
27
+ <div data-testid="mobile-width">{layoutState.mobileWidth}</div>
28
+ <div data-testid="is-fullscreen">{String(layoutState.isFullscreen)}</div>
29
+ <div data-testid="is-resizing">{String(layoutState.isResizing)}</div>
30
+ <div data-testid="is-mobile-view">{String(layoutState.isMobileView)}</div>
31
+ <div data-testid="is-desktop-view">{String(layoutState.isDesktopView)}</div>
32
+
33
+ <button onClick={() => layoutState.setSplitSize(60)} data-testid="set-split">
34
+ Set Split
35
+ </button>
36
+ <button onClick={() => layoutState.setViewMode(PREVIEW_MODES.MOBILE)} data-testid="set-mobile">
37
+ Set Mobile
38
+ </button>
39
+ <button onClick={() => layoutState.toggleViewMode()} data-testid="toggle-view">
40
+ Toggle View
41
+ </button>
42
+ <button onClick={() => layoutState.setMobileWidth(400)} data-testid="set-width">
43
+ Set Width
44
+ </button>
45
+ <button onClick={() => layoutState.toggleFullscreen()} data-testid="toggle-fullscreen">
46
+ Toggle Fullscreen
47
+ </button>
48
+ <button onClick={() => layoutState.resetLayout()} data-testid="reset">
49
+ Reset
50
+ </button>
51
+ <button onClick={() => layoutState.setResizingState(true)} data-testid="start-resize">
52
+ Start Resize
53
+ </button>
54
+ <button onClick={() => layoutState.updateSplitSizes([40, 60])} data-testid="update-sizes">
55
+ Update Sizes
56
+ </button>
57
+ </div>
58
+ );
59
+ };
60
+
61
+ describe('useLayoutState', () => {
62
+ beforeEach(() => {
63
+ // Clear any existing event listeners
64
+ jest.clearAllMocks();
65
+ });
66
+
67
+ afterEach(() => {
68
+ // Cleanup
69
+ jest.restoreAllMocks();
70
+ });
71
+
72
+ describe('Initial State', () => {
73
+ it('initializes with default layout values', () => {
74
+ render(<TestComponent />);
75
+
76
+ expect(screen.getByTestId('split-size')).toHaveTextContent('50');
77
+ expect(screen.getByTestId('view-mode')).toHaveTextContent(PREVIEW_MODES.DESKTOP);
78
+ expect(screen.getByTestId('mobile-width')).toHaveTextContent(String(LAYOUT.MOBILE_WIDTH_DEFAULT));
79
+ expect(screen.getByTestId('is-fullscreen')).toHaveTextContent('false');
80
+ expect(screen.getByTestId('is-resizing')).toHaveTextContent('false');
81
+ });
82
+
83
+ it('accepts custom initial layout', () => {
84
+ const customLayout = {
85
+ splitSize: 30,
86
+ viewMode: PREVIEW_MODES.MOBILE,
87
+ mobileWidth: 400,
88
+ isFullscreen: true
89
+ };
90
+
91
+ render(<TestComponent initialLayout={customLayout} />);
92
+
93
+ expect(screen.getByTestId('split-size')).toHaveTextContent('30');
94
+ expect(screen.getByTestId('view-mode')).toHaveTextContent(PREVIEW_MODES.MOBILE);
95
+ expect(screen.getByTestId('mobile-width')).toHaveTextContent('400');
96
+ expect(screen.getByTestId('is-fullscreen')).toHaveTextContent('true');
97
+ });
98
+
99
+ it('computes isMobileView correctly', () => {
100
+ render(<TestComponent initialLayout={{ viewMode: PREVIEW_MODES.MOBILE }} />);
101
+
102
+ expect(screen.getByTestId('is-mobile-view')).toHaveTextContent('true');
103
+ expect(screen.getByTestId('is-desktop-view')).toHaveTextContent('false');
104
+ });
105
+
106
+ it('computes isDesktopView correctly', () => {
107
+ render(<TestComponent initialLayout={{ viewMode: PREVIEW_MODES.DESKTOP }} />);
108
+
109
+ expect(screen.getByTestId('is-mobile-view')).toHaveTextContent('false');
110
+ expect(screen.getByTestId('is-desktop-view')).toHaveTextContent('true');
111
+ });
112
+ });
113
+
114
+ describe('setSplitSize', () => {
115
+ it('updates split size', () => {
116
+ render(<TestComponent />);
117
+
118
+ act(() => {
119
+ fireEvent.click(screen.getByTestId('set-split'));
120
+ });
121
+
122
+ expect(screen.getByTestId('split-size')).toHaveTextContent('60');
123
+ });
124
+
125
+ it('rejects split size below 10', () => {
126
+ let layoutState;
127
+ render(<TestComponent onStateChange={(state) => { layoutState = state; }} />);
128
+
129
+ act(() => {
130
+ layoutState.setSplitSize(5);
131
+ });
132
+
133
+ expect(screen.getByTestId('split-size')).toHaveTextContent('50'); // Should remain unchanged
134
+ });
135
+
136
+ it('rejects split size above 90', () => {
137
+ let layoutState;
138
+ render(<TestComponent onStateChange={(state) => { layoutState = state; }} />);
139
+
140
+ act(() => {
141
+ layoutState.setSplitSize(95);
142
+ });
143
+
144
+ expect(screen.getByTestId('split-size')).toHaveTextContent('50'); // Should remain unchanged
145
+ });
146
+
147
+ it('updates splitSizes array when split size changes', () => {
148
+ let layoutState;
149
+ render(<TestComponent onStateChange={(state) => { layoutState = state; }} />);
150
+
151
+ act(() => {
152
+ layoutState.setSplitSize(70);
153
+ });
154
+
155
+ expect(layoutState.splitSizes).toEqual([70, 30]);
156
+ });
157
+ });
158
+
159
+ describe('setViewMode', () => {
160
+ it('changes view mode to mobile', () => {
161
+ render(<TestComponent />);
162
+
163
+ act(() => {
164
+ fireEvent.click(screen.getByTestId('set-mobile'));
165
+ });
166
+
167
+ expect(screen.getByTestId('view-mode')).toHaveTextContent(PREVIEW_MODES.MOBILE);
168
+ });
169
+
170
+ it('ignores invalid view modes', () => {
171
+ let layoutState;
172
+ render(<TestComponent onStateChange={(state) => { layoutState = state; }} />);
173
+
174
+ act(() => {
175
+ layoutState.setViewMode('invalid-mode');
176
+ });
177
+
178
+ expect(screen.getByTestId('view-mode')).toHaveTextContent(PREVIEW_MODES.DESKTOP); // Should remain unchanged
179
+ });
180
+
181
+ it('accepts all valid preview modes', () => {
182
+ let layoutState;
183
+ render(<TestComponent onStateChange={(state) => { layoutState = state; }} />);
184
+
185
+ Object.values(PREVIEW_MODES).forEach(mode => {
186
+ act(() => {
187
+ layoutState.setViewMode(mode);
188
+ });
189
+ expect(screen.getByTestId('view-mode')).toHaveTextContent(mode);
190
+ });
191
+ });
192
+ });
193
+
194
+ describe('toggleViewMode', () => {
195
+ it('toggles from desktop to mobile', () => {
196
+ render(<TestComponent initialLayout={{ viewMode: PREVIEW_MODES.DESKTOP }} />);
197
+
198
+ act(() => {
199
+ fireEvent.click(screen.getByTestId('toggle-view'));
200
+ });
201
+
202
+ expect(screen.getByTestId('view-mode')).toHaveTextContent(PREVIEW_MODES.MOBILE);
203
+ });
204
+
205
+ it('toggles from mobile to desktop', () => {
206
+ render(<TestComponent initialLayout={{ viewMode: PREVIEW_MODES.MOBILE }} />);
207
+
208
+ act(() => {
209
+ fireEvent.click(screen.getByTestId('toggle-view'));
210
+ });
211
+
212
+ expect(screen.getByTestId('view-mode')).toHaveTextContent(PREVIEW_MODES.DESKTOP);
213
+ });
214
+
215
+ it('toggles multiple times correctly', () => {
216
+ render(<TestComponent />);
217
+
218
+ act(() => {
219
+ fireEvent.click(screen.getByTestId('toggle-view'));
220
+ });
221
+ expect(screen.getByTestId('view-mode')).toHaveTextContent(PREVIEW_MODES.MOBILE);
222
+
223
+ act(() => {
224
+ fireEvent.click(screen.getByTestId('toggle-view'));
225
+ });
226
+ expect(screen.getByTestId('view-mode')).toHaveTextContent(PREVIEW_MODES.DESKTOP);
227
+ });
228
+ });
229
+
230
+ describe('setMobileWidth', () => {
231
+ it('updates mobile width', () => {
232
+ render(<TestComponent />);
233
+
234
+ act(() => {
235
+ fireEvent.click(screen.getByTestId('set-width'));
236
+ });
237
+
238
+ expect(screen.getByTestId('mobile-width')).toHaveTextContent('400');
239
+ });
240
+
241
+ it('constrains width to minimum 320', () => {
242
+ let layoutState;
243
+ render(<TestComponent onStateChange={(state) => { layoutState = state; }} />);
244
+
245
+ act(() => {
246
+ layoutState.setMobileWidth(200);
247
+ });
248
+
249
+ expect(screen.getByTestId('mobile-width')).toHaveTextContent('320');
250
+ });
251
+
252
+ it('constrains width to maximum 768', () => {
253
+ let layoutState;
254
+ render(<TestComponent onStateChange={(state) => { layoutState = state; }} />);
255
+
256
+ act(() => {
257
+ layoutState.setMobileWidth(1000);
258
+ });
259
+
260
+ expect(screen.getByTestId('mobile-width')).toHaveTextContent('768');
261
+ });
262
+
263
+ it('accepts width within valid range', () => {
264
+ let layoutState;
265
+ render(<TestComponent onStateChange={(state) => { layoutState = state; }} />);
266
+
267
+ const validWidths = [320, 375, 400, 500, 600, 768];
268
+ validWidths.forEach(width => {
269
+ act(() => {
270
+ layoutState.setMobileWidth(width);
271
+ });
272
+ expect(screen.getByTestId('mobile-width')).toHaveTextContent(String(width));
273
+ });
274
+ });
275
+ });
276
+
277
+ describe('toggleFullscreen', () => {
278
+ it('toggles fullscreen on', () => {
279
+ render(<TestComponent />);
280
+
281
+ act(() => {
282
+ fireEvent.click(screen.getByTestId('toggle-fullscreen'));
283
+ });
284
+
285
+ expect(screen.getByTestId('is-fullscreen')).toHaveTextContent('true');
286
+ });
287
+
288
+ it('toggles fullscreen off', () => {
289
+ render(<TestComponent initialLayout={{ isFullscreen: true }} />);
290
+
291
+ act(() => {
292
+ fireEvent.click(screen.getByTestId('toggle-fullscreen'));
293
+ });
294
+
295
+ expect(screen.getByTestId('is-fullscreen')).toHaveTextContent('false');
296
+ });
297
+
298
+ it('toggles multiple times', () => {
299
+ render(<TestComponent />);
300
+
301
+ act(() => {
302
+ fireEvent.click(screen.getByTestId('toggle-fullscreen'));
303
+ });
304
+ expect(screen.getByTestId('is-fullscreen')).toHaveTextContent('true');
305
+
306
+ act(() => {
307
+ fireEvent.click(screen.getByTestId('toggle-fullscreen'));
308
+ });
309
+ expect(screen.getByTestId('is-fullscreen')).toHaveTextContent('false');
310
+ });
311
+ });
312
+
313
+ describe('resetLayout', () => {
314
+ it('resets layout to initial defaults', () => {
315
+ // Note: resetLayout resets to the initial layout provided to the hook
316
+ // If custom initial layout was provided, it resets to that
317
+ const initialLayout = {
318
+ splitSize: 70,
319
+ viewMode: PREVIEW_MODES.MOBILE,
320
+ mobileWidth: 600,
321
+ isFullscreen: true
322
+ };
323
+
324
+ let layoutState;
325
+ render(<TestComponent
326
+ initialLayout={initialLayout}
327
+ onStateChange={(state) => { layoutState = state; }}
328
+ />);
329
+
330
+ // Change some values
331
+ act(() => {
332
+ layoutState.setSplitSize(30);
333
+ layoutState.setViewMode(PREVIEW_MODES.DESKTOP);
334
+ });
335
+
336
+ // Verify values changed
337
+ expect(screen.getByTestId('split-size')).toHaveTextContent('30');
338
+ expect(screen.getByTestId('view-mode')).toHaveTextContent(PREVIEW_MODES.DESKTOP);
339
+
340
+ // Reset should go back to initial values
341
+ act(() => {
342
+ fireEvent.click(screen.getByTestId('reset'));
343
+ });
344
+
345
+ expect(screen.getByTestId('split-size')).toHaveTextContent('70');
346
+ expect(screen.getByTestId('view-mode')).toHaveTextContent(PREVIEW_MODES.MOBILE);
347
+ expect(screen.getByTestId('mobile-width')).toHaveTextContent('600');
348
+ expect(screen.getByTestId('is-fullscreen')).toHaveTextContent('true');
349
+ });
350
+ });
351
+
352
+ describe('setResizingState', () => {
353
+ it('sets resizing to true', () => {
354
+ render(<TestComponent />);
355
+
356
+ act(() => {
357
+ fireEvent.click(screen.getByTestId('start-resize'));
358
+ });
359
+
360
+ expect(screen.getByTestId('is-resizing')).toHaveTextContent('true');
361
+ });
362
+
363
+ it('sets resizing to false', () => {
364
+ let layoutState;
365
+ render(<TestComponent onStateChange={(state) => { layoutState = state; }} />);
366
+
367
+ act(() => {
368
+ layoutState.setResizingState(true);
369
+ });
370
+ expect(screen.getByTestId('is-resizing')).toHaveTextContent('true');
371
+
372
+ act(() => {
373
+ layoutState.setResizingState(false);
374
+ });
375
+ expect(screen.getByTestId('is-resizing')).toHaveTextContent('false');
376
+ });
377
+ });
378
+
379
+ describe('updateSplitSizes', () => {
380
+ it('updates split sizes array', () => {
381
+ let layoutState;
382
+ render(<TestComponent onStateChange={(state) => { layoutState = state; }} />);
383
+
384
+ act(() => {
385
+ fireEvent.click(screen.getByTestId('update-sizes'));
386
+ });
387
+
388
+ expect(layoutState.splitSizes).toEqual([40, 60]);
389
+ expect(screen.getByTestId('split-size')).toHaveTextContent('40');
390
+ });
391
+
392
+ it('rejects sizes below minimum', () => {
393
+ let layoutState;
394
+ render(<TestComponent onStateChange={(state) => { layoutState = state; }} />);
395
+
396
+ act(() => {
397
+ layoutState.updateSplitSizes([10, 90]);
398
+ });
399
+
400
+ expect(screen.getByTestId('split-size')).toHaveTextContent('50'); // Should remain unchanged
401
+ });
402
+
403
+ it('rejects sizes above maximum', () => {
404
+ let layoutState;
405
+ render(<TestComponent onStateChange={(state) => { layoutState = state; }} />);
406
+
407
+ act(() => {
408
+ layoutState.updateSplitSizes([85, 15]);
409
+ });
410
+
411
+ expect(screen.getByTestId('split-size')).toHaveTextContent('50'); // Should remain unchanged
412
+ });
413
+
414
+ it('rejects sizes that do not add up to 100', () => {
415
+ let layoutState;
416
+ render(<TestComponent onStateChange={(state) => { layoutState = state; }} />);
417
+
418
+ act(() => {
419
+ layoutState.updateSplitSizes([50, 45]);
420
+ });
421
+
422
+ expect(screen.getByTestId('split-size')).toHaveTextContent('50'); // Should remain unchanged
423
+ });
424
+ });
425
+
426
+ describe('handleResize', () => {
427
+ it('calculates new sizes based on delta', () => {
428
+ let layoutState;
429
+ render(<TestComponent onStateChange={(state) => { layoutState = state; }} />);
430
+
431
+ act(() => {
432
+ layoutState.handleResize(100, 1000); // 10% increase
433
+ });
434
+
435
+ expect(layoutState.splitSizes[0]).toBeCloseTo(60, 0);
436
+ expect(layoutState.splitSizes[1]).toBeCloseTo(40, 0);
437
+ });
438
+
439
+ it('respects minimum pane size constraints', () => {
440
+ let layoutState;
441
+ render(<TestComponent
442
+ initialLayout={{ splitSizes: [25, 75], splitSize: 25 }}
443
+ onStateChange={(state) => { layoutState = state; }}
444
+ />);
445
+
446
+ act(() => {
447
+ layoutState.handleResize(-100, 1000); // Try to shrink below min
448
+ });
449
+
450
+ expect(layoutState.splitSizes[0]).toBeGreaterThanOrEqual(LAYOUT.MIN_PANE_SIZE);
451
+ });
452
+
453
+ it('ignores zero container size', () => {
454
+ let layoutState;
455
+ render(<TestComponent onStateChange={(state) => { layoutState = state; }} />);
456
+
457
+ const initialSizes = [...layoutState.splitSizes];
458
+
459
+ act(() => {
460
+ layoutState.handleResize(100, 0);
461
+ });
462
+
463
+ expect(layoutState.splitSizes).toEqual(initialSizes); // Should remain unchanged
464
+ });
465
+ });
466
+
467
+ describe('Computed Properties', () => {
468
+ it('exposes layout constraints', () => {
469
+ let layoutState;
470
+ render(<TestComponent onStateChange={(state) => { layoutState = state; }} />);
471
+
472
+ expect(layoutState.minPaneSize).toBe(LAYOUT.MIN_PANE_SIZE);
473
+ expect(layoutState.maxPaneSize).toBe(LAYOUT.MAX_PANE_SIZE);
474
+ expect(layoutState.gutterSize).toBe(LAYOUT.GUTTER_SIZE);
475
+ });
476
+ });
477
+
478
+ describe('Keyboard Shortcuts', () => {
479
+ it('handles F11 for fullscreen', () => {
480
+ render(<TestComponent />);
481
+
482
+ act(() => {
483
+ const event = new KeyboardEvent('keydown', { key: 'F11' });
484
+ document.dispatchEvent(event);
485
+ });
486
+
487
+ expect(screen.getByTestId('is-fullscreen')).toHaveTextContent('true');
488
+ });
489
+
490
+ it('handles Ctrl+P for view toggle', () => {
491
+ render(<TestComponent />);
492
+
493
+ act(() => {
494
+ const event = new KeyboardEvent('keydown', { key: 'p', ctrlKey: true });
495
+ document.dispatchEvent(event);
496
+ });
497
+
498
+ expect(screen.getByTestId('view-mode')).toHaveTextContent(PREVIEW_MODES.MOBILE);
499
+ });
500
+
501
+ it('handles Cmd+P for view toggle on Mac', () => {
502
+ render(<TestComponent />);
503
+
504
+ act(() => {
505
+ const event = new KeyboardEvent('keydown', { key: 'p', metaKey: true });
506
+ document.dispatchEvent(event);
507
+ });
508
+
509
+ expect(screen.getByTestId('view-mode')).toHaveTextContent(PREVIEW_MODES.MOBILE);
510
+ });
511
+ });
512
+
513
+ describe('Responsive Behavior', () => {
514
+ it('switches to mobile view on small screens', () => {
515
+ // Mock window.innerWidth
516
+ Object.defineProperty(window, 'innerWidth', {
517
+ writable: true,
518
+ configurable: true,
519
+ value: 500 // Below MOBILE_BREAKPOINT
520
+ });
521
+
522
+ render(<TestComponent initialLayout={{ viewMode: PREVIEW_MODES.DESKTOP }} />);
523
+
524
+ act(() => {
525
+ window.dispatchEvent(new Event('resize'));
526
+ });
527
+
528
+ expect(screen.getByTestId('view-mode')).toHaveTextContent(PREVIEW_MODES.MOBILE);
529
+ });
530
+
531
+ it('does not auto-switch when already in mobile view', () => {
532
+ Object.defineProperty(window, 'innerWidth', {
533
+ writable: true,
534
+ configurable: true,
535
+ value: 500
536
+ });
537
+
538
+ render(<TestComponent initialLayout={{ viewMode: PREVIEW_MODES.MOBILE }} />);
539
+
540
+ act(() => {
541
+ window.dispatchEvent(new Event('resize'));
542
+ });
543
+
544
+ expect(screen.getByTestId('view-mode')).toHaveTextContent(PREVIEW_MODES.MOBILE);
545
+ });
546
+ });
547
+
548
+ describe('Edge Cases', () => {
549
+ it('handles boundary split sizes', () => {
550
+ let layoutState;
551
+ render(<TestComponent onStateChange={(state) => { layoutState = state; }} />);
552
+
553
+ act(() => {
554
+ layoutState.setSplitSize(10); // Minimum
555
+ });
556
+ expect(screen.getByTestId('split-size')).toHaveTextContent('10');
557
+
558
+ act(() => {
559
+ layoutState.setSplitSize(90); // Maximum
560
+ });
561
+ expect(screen.getByTestId('split-size')).toHaveTextContent('90');
562
+ });
563
+
564
+ it('handles exact constraint boundaries for mobile width', () => {
565
+ let layoutState;
566
+ render(<TestComponent onStateChange={(state) => { layoutState = state; }} />);
567
+
568
+ act(() => {
569
+ layoutState.setMobileWidth(320); // Min
570
+ });
571
+ expect(screen.getByTestId('mobile-width')).toHaveTextContent('320');
572
+
573
+ act(() => {
574
+ layoutState.setMobileWidth(768); // Max
575
+ });
576
+ expect(screen.getByTestId('mobile-width')).toHaveTextContent('768');
577
+ });
578
+ });
579
+ });
580
+