@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,1109 @@
1
+ /**
2
+ * simplePerformance Tests
3
+ *
4
+ * Tests for performance optimization utilities
5
+ */
6
+
7
+ import React from 'react';
8
+ import { render, screen } from '@testing-library/react';
9
+ import '@testing-library/jest-dom';
10
+ import { renderHook, act } from '@testing-library/react-hooks';
11
+ import {
12
+ useSmartDebounce,
13
+ withPerformanceOptimization,
14
+ useCleanup,
15
+ analyzeContentSize,
16
+ useVirtualization,
17
+ PERFORMANCE_THRESHOLDS
18
+ } from '../simplePerformance';
19
+
20
+ describe('simplePerformance', () => {
21
+ beforeEach(() => {
22
+ jest.useFakeTimers();
23
+ });
24
+
25
+ afterEach(() => {
26
+ jest.clearAllTimers();
27
+ jest.useRealTimers();
28
+ });
29
+
30
+ describe('useSmartDebounce', () => {
31
+ it('debounces callback execution', () => {
32
+ const callback = jest.fn();
33
+ const { result } = renderHook(() => useSmartDebounce(callback, 300));
34
+
35
+ act(() => {
36
+ result.current('test value');
37
+ });
38
+
39
+ expect(callback).not.toHaveBeenCalled();
40
+
41
+ act(() => {
42
+ jest.advanceTimersByTime(300);
43
+ });
44
+
45
+ expect(callback).toHaveBeenCalledWith('test value');
46
+ expect(callback).toHaveBeenCalledTimes(1);
47
+ });
48
+
49
+ it('adjusts delay for small content (< 1KB)', () => {
50
+ const callback = jest.fn();
51
+ const { result } = renderHook(() => useSmartDebounce(callback, 300));
52
+
53
+ act(() => {
54
+ result.current('test', 500); // Small content (< 1KB)
55
+ });
56
+
57
+ // Should use 200ms delay for small content (Math.round(300 * 2/3) = 200ms)
58
+ act(() => {
59
+ jest.advanceTimersByTime(199);
60
+ });
61
+ expect(callback).not.toHaveBeenCalled();
62
+
63
+ act(() => {
64
+ jest.advanceTimersByTime(1);
65
+ });
66
+ expect(callback).toHaveBeenCalled();
67
+ });
68
+
69
+ it('handles boundary test at exactly SMALL_CONTENT_DEBOUNCE threshold', () => {
70
+ const callback = jest.fn();
71
+ const { result } = renderHook(() => useSmartDebounce(callback, 300));
72
+
73
+ // Test exactly at threshold (should use base delay, not small content delay)
74
+ act(() => {
75
+ result.current('test', PERFORMANCE_THRESHOLDS.SMALL_CONTENT_DEBOUNCE);
76
+ });
77
+
78
+ // Should use base delay (300ms) for content >= SMALL_CONTENT_DEBOUNCE
79
+ act(() => {
80
+ jest.advanceTimersByTime(299);
81
+ });
82
+ expect(callback).not.toHaveBeenCalled();
83
+
84
+ act(() => {
85
+ jest.advanceTimersByTime(1);
86
+ });
87
+ expect(callback).toHaveBeenCalled();
88
+ });
89
+
90
+ it('handles boundary test just below SMALL_CONTENT_DEBOUNCE threshold', () => {
91
+ const callback = jest.fn();
92
+ const { result } = renderHook(() => useSmartDebounce(callback, 300));
93
+
94
+ // Test just below threshold (should use small content delay)
95
+ act(() => {
96
+ result.current('test', PERFORMANCE_THRESHOLDS.SMALL_CONTENT_DEBOUNCE - 1);
97
+ });
98
+
99
+ // Should use 200ms delay for small content (< SMALL_CONTENT_DEBOUNCE)
100
+ act(() => {
101
+ jest.advanceTimersByTime(199);
102
+ });
103
+ expect(callback).not.toHaveBeenCalled();
104
+
105
+ act(() => {
106
+ jest.advanceTimersByTime(1);
107
+ });
108
+ expect(callback).toHaveBeenCalled();
109
+ });
110
+
111
+ it('adjusts delay for medium content (10KB - 50KB)', () => {
112
+ const callback = jest.fn();
113
+ const { result } = renderHook(() => useSmartDebounce(callback, 300));
114
+
115
+ act(() => {
116
+ result.current('test', 25000); // 25KB
117
+ });
118
+
119
+ // Should use 399ms delay for medium content (Math.round(300 * 1.33) = 399ms)
120
+ act(() => {
121
+ jest.advanceTimersByTime(398);
122
+ });
123
+ expect(callback).not.toHaveBeenCalled();
124
+
125
+ act(() => {
126
+ jest.advanceTimersByTime(1);
127
+ });
128
+ expect(callback).toHaveBeenCalled();
129
+ });
130
+
131
+ it('adjusts delay for large content (> 50KB)', () => {
132
+ const callback = jest.fn();
133
+ const { result } = renderHook(() => useSmartDebounce(callback, 300));
134
+
135
+ act(() => {
136
+ result.current('test', 75000); // 75KB
137
+ });
138
+
139
+ // Should use 600ms delay for large content
140
+ act(() => {
141
+ jest.advanceTimersByTime(599);
142
+ });
143
+ expect(callback).not.toHaveBeenCalled();
144
+
145
+ act(() => {
146
+ jest.advanceTimersByTime(1);
147
+ });
148
+ expect(callback).toHaveBeenCalled();
149
+ });
150
+
151
+ it('cancels previous timeout on new call', () => {
152
+ const callback = jest.fn();
153
+ const { result } = renderHook(() => useSmartDebounce(callback, 300));
154
+
155
+ act(() => {
156
+ result.current('first');
157
+ });
158
+
159
+ act(() => {
160
+ jest.advanceTimersByTime(100);
161
+ });
162
+
163
+ act(() => {
164
+ result.current('second');
165
+ });
166
+
167
+ act(() => {
168
+ jest.advanceTimersByTime(300);
169
+ });
170
+
171
+ expect(callback).toHaveBeenCalledTimes(1);
172
+ expect(callback).toHaveBeenCalledWith('second');
173
+ });
174
+
175
+ it('respects custom base delay', () => {
176
+ const callback = jest.fn();
177
+ const { result } = renderHook(() => useSmartDebounce(callback, 500));
178
+
179
+ act(() => {
180
+ result.current('test', 5000); // Medium content (1KB-10KB range, uses baseDelay as-is)
181
+ });
182
+
183
+ act(() => {
184
+ jest.advanceTimersByTime(499); // Should use 500ms (custom baseDelay)
185
+ });
186
+ expect(callback).not.toHaveBeenCalled();
187
+
188
+ act(() => {
189
+ jest.advanceTimersByTime(1);
190
+ });
191
+ expect(callback).toHaveBeenCalled();
192
+ });
193
+
194
+ it('handles zero content size', () => {
195
+ const callback = jest.fn();
196
+ const { result } = renderHook(() => useSmartDebounce(callback, 300));
197
+
198
+ act(() => {
199
+ result.current('test', 0);
200
+ });
201
+
202
+ // Zero content size should use reduced delay (Math.round(300 * 2/3) = 200ms)
203
+ act(() => {
204
+ jest.advanceTimersByTime(199);
205
+ });
206
+ expect(callback).not.toHaveBeenCalled();
207
+
208
+ act(() => {
209
+ jest.advanceTimersByTime(1);
210
+ });
211
+ expect(callback).toHaveBeenCalled();
212
+ });
213
+ });
214
+
215
+ describe('withPerformanceOptimization', () => {
216
+ const TestComponent = ({ content, variant, readOnly, testId }) => (
217
+ <div data-testid={testId || 'test-component'}>
218
+ Content: {content}, Variant: {variant}, ReadOnly: {readOnly ? 'true' : 'false'}
219
+ </div>
220
+ );
221
+
222
+ it('creates a memoized component', () => {
223
+ const OptimizedComponent = withPerformanceOptimization(TestComponent);
224
+
225
+ expect(OptimizedComponent).toBeDefined();
226
+ expect(typeof OptimizedComponent).toBe('object'); // React.memo returns an object
227
+ expect(OptimizedComponent.$$typeof).toBeDefined(); // React component symbol
228
+ });
229
+
230
+ it('prevents re-render when props have not changed', () => {
231
+ const OptimizedComponent = withPerformanceOptimization(TestComponent);
232
+ const renderSpy = jest.fn();
233
+
234
+ const SpyComponent = (props) => {
235
+ renderSpy();
236
+ return <TestComponent {...props} />;
237
+ };
238
+
239
+ const OptimizedSpyComponent = withPerformanceOptimization(SpyComponent);
240
+
241
+ const { rerender } = render(
242
+ <OptimizedSpyComponent
243
+ content="test content"
244
+ variant="email"
245
+ readOnly={false}
246
+ />
247
+ );
248
+
249
+ expect(renderSpy).toHaveBeenCalledTimes(1);
250
+
251
+ // Re-render with same props
252
+ rerender(
253
+ <OptimizedSpyComponent
254
+ content="test content"
255
+ variant="email"
256
+ readOnly={false}
257
+ />
258
+ );
259
+
260
+ expect(renderSpy).toHaveBeenCalledTimes(1); // Should not re-render
261
+ });
262
+
263
+ it('allows re-render when content changes', () => {
264
+ const OptimizedComponent = withPerformanceOptimization(TestComponent);
265
+
266
+ const { rerender } = render(
267
+ <OptimizedComponent
268
+ content="original content"
269
+ variant="email"
270
+ readOnly={false}
271
+ />
272
+ );
273
+
274
+ expect(screen.getByText('Content: original content, Variant: email, ReadOnly: false')).toBeInTheDocument();
275
+
276
+ rerender(
277
+ <OptimizedComponent
278
+ content="updated content"
279
+ variant="email"
280
+ readOnly={false}
281
+ />
282
+ );
283
+
284
+ expect(screen.getByText('Content: updated content, Variant: email, ReadOnly: false')).toBeInTheDocument();
285
+ });
286
+
287
+ it('allows re-render when variant changes', () => {
288
+ const OptimizedComponent = withPerformanceOptimization(TestComponent);
289
+
290
+ const { rerender } = render(
291
+ <OptimizedComponent
292
+ content="test content"
293
+ variant="email"
294
+ readOnly={false}
295
+ />
296
+ );
297
+
298
+ expect(screen.getByText('Content: test content, Variant: email, ReadOnly: false')).toBeInTheDocument();
299
+
300
+ rerender(
301
+ <OptimizedComponent
302
+ content="test content"
303
+ variant="inapp"
304
+ readOnly={false}
305
+ />
306
+ );
307
+
308
+ expect(screen.getByText('Content: test content, Variant: inapp, ReadOnly: false')).toBeInTheDocument();
309
+ });
310
+
311
+ it('allows re-render when readOnly changes', () => {
312
+ const OptimizedComponent = withPerformanceOptimization(TestComponent);
313
+
314
+ const { rerender } = render(
315
+ <OptimizedComponent
316
+ content="test content"
317
+ variant="email"
318
+ readOnly={false}
319
+ />
320
+ );
321
+
322
+ expect(screen.getByText('Content: test content, Variant: email, ReadOnly: false')).toBeInTheDocument();
323
+
324
+ rerender(
325
+ <OptimizedComponent
326
+ content="test content"
327
+ variant="email"
328
+ readOnly={true}
329
+ />
330
+ );
331
+
332
+ expect(screen.getByText('Content: test content, Variant: email, ReadOnly: true')).toBeInTheDocument();
333
+ });
334
+
335
+ it('handles multiple prop changes', () => {
336
+ const OptimizedComponent = withPerformanceOptimization(TestComponent);
337
+
338
+ const { rerender } = render(
339
+ <OptimizedComponent
340
+ content="original"
341
+ variant="email"
342
+ readOnly={false}
343
+ />
344
+ );
345
+
346
+ rerender(
347
+ <OptimizedComponent
348
+ content="updated"
349
+ variant="inapp"
350
+ readOnly={true}
351
+ />
352
+ );
353
+
354
+ expect(screen.getByText('Content: updated, Variant: inapp, ReadOnly: true')).toBeInTheDocument();
355
+ });
356
+
357
+ it('ignores other props changes', () => {
358
+ const renderSpy = jest.fn();
359
+
360
+ const SpyComponent = (props) => {
361
+ renderSpy();
362
+ return <TestComponent {...props} />;
363
+ };
364
+
365
+ const OptimizedComponent = withPerformanceOptimization(SpyComponent);
366
+
367
+ const { rerender } = render(
368
+ <OptimizedComponent
369
+ content="test content"
370
+ variant="email"
371
+ readOnly={false}
372
+ otherProp="value1"
373
+ />
374
+ );
375
+
376
+ expect(renderSpy).toHaveBeenCalledTimes(1);
377
+
378
+ // Change only otherProp
379
+ rerender(
380
+ <OptimizedComponent
381
+ content="test content"
382
+ variant="email"
383
+ readOnly={false}
384
+ otherProp="value2"
385
+ />
386
+ );
387
+
388
+ expect(renderSpy).toHaveBeenCalledTimes(1); // Should not re-render
389
+ });
390
+
391
+ it('handles undefined props gracefully', () => {
392
+ const OptimizedComponent = withPerformanceOptimization(TestComponent);
393
+
394
+ expect(() => {
395
+ render(
396
+ <OptimizedComponent
397
+ content={undefined}
398
+ variant={undefined}
399
+ readOnly={undefined}
400
+ />
401
+ );
402
+ }).not.toThrow();
403
+ });
404
+
405
+ it('maintains component functionality', () => {
406
+ const OptimizedComponent = withPerformanceOptimization(TestComponent);
407
+
408
+ render(
409
+ <OptimizedComponent
410
+ content="functional test"
411
+ variant="email"
412
+ readOnly={true}
413
+ testId="optimized-component"
414
+ />
415
+ );
416
+
417
+ expect(screen.getByTestId('optimized-component')).toBeInTheDocument();
418
+ expect(screen.getByText(/Content: functional test/)).toBeInTheDocument();
419
+ });
420
+ });
421
+
422
+ describe('useCleanup', () => {
423
+ it('calls cleanup function on unmount', () => {
424
+ const cleanup = jest.fn();
425
+ const { unmount } = renderHook(() => useCleanup(cleanup));
426
+
427
+ expect(cleanup).not.toHaveBeenCalled();
428
+
429
+ unmount();
430
+
431
+ expect(cleanup).toHaveBeenCalledTimes(1);
432
+ });
433
+
434
+ it('handles multiple mount/unmount cycles', () => {
435
+ const cleanup = jest.fn();
436
+ const { unmount, rerender } = renderHook(() => useCleanup(cleanup));
437
+
438
+ rerender();
439
+ expect(cleanup).not.toHaveBeenCalled();
440
+
441
+ unmount();
442
+ expect(cleanup).toHaveBeenCalledTimes(1);
443
+ });
444
+ });
445
+
446
+ describe('analyzeContentSize', () => {
447
+ it('analyzes small content (< 10KB)', () => {
448
+ const content = 'a'.repeat(5000);
449
+ const result = analyzeContentSize(content);
450
+
451
+ expect(result.size).toBe(5000);
452
+ expect(result.isSmall).toBe(true);
453
+ expect(result.isMedium).toBe(true);
454
+ expect(result.isLarge).toBe(false);
455
+ expect(result.shouldOptimize).toBe(false);
456
+ expect(result.recommendedDebounce).toBe(200);
457
+ });
458
+
459
+ it('analyzes medium content (10KB - 50KB)', () => {
460
+ const content = 'a'.repeat(25000);
461
+ const result = analyzeContentSize(content);
462
+
463
+ expect(result.size).toBe(25000);
464
+ expect(result.isSmall).toBe(false);
465
+ expect(result.isMedium).toBe(true);
466
+ expect(result.isLarge).toBe(false);
467
+ expect(result.shouldOptimize).toBe(true);
468
+ expect(result.recommendedDebounce).toBe(400);
469
+ });
470
+
471
+ it('analyzes large content (>= 50KB)', () => {
472
+ const content = 'a'.repeat(75000);
473
+ const result = analyzeContentSize(content);
474
+
475
+ expect(result.size).toBe(75000);
476
+ expect(result.isSmall).toBe(false);
477
+ expect(result.isMedium).toBe(false);
478
+ expect(result.isLarge).toBe(true);
479
+ expect(result.shouldOptimize).toBe(true);
480
+ expect(result.recommendedDebounce).toBe(600);
481
+ });
482
+
483
+ it('handles empty content', () => {
484
+ const result = analyzeContentSize('');
485
+
486
+ expect(result.size).toBe(0);
487
+ expect(result.isSmall).toBe(true);
488
+ expect(result.shouldOptimize).toBe(false);
489
+ });
490
+
491
+ it('handles null content', () => {
492
+ const result = analyzeContentSize(null);
493
+
494
+ expect(result.size).toBe(0);
495
+ expect(result.isSmall).toBe(true);
496
+ });
497
+
498
+ it('handles undefined content', () => {
499
+ const result = analyzeContentSize(undefined);
500
+
501
+ expect(result.size).toBe(0);
502
+ expect(result.isSmall).toBe(true);
503
+ });
504
+
505
+ it('handles non-string content', () => {
506
+ const result = analyzeContentSize(12345);
507
+
508
+ expect(result.size).toBe(0);
509
+ expect(result.isSmall).toBe(true);
510
+ });
511
+
512
+ it('provides correct size thresholds', () => {
513
+ const exactly10KB = analyzeContentSize('a'.repeat(10000));
514
+ expect(exactly10KB.isSmall).toBe(false);
515
+ expect(exactly10KB.shouldOptimize).toBe(false);
516
+
517
+ const exactly50KB = analyzeContentSize('a'.repeat(50000));
518
+ expect(exactly50KB.isMedium).toBe(false);
519
+ expect(exactly50KB.isLarge).toBe(true);
520
+ });
521
+ });
522
+
523
+ describe('useVirtualization', () => {
524
+ it('returns content as-is for small content', () => {
525
+ const content = 'a'.repeat(5000);
526
+ const { result } = renderHook(() => useVirtualization(content, 100000));
527
+
528
+ expect(result.current.content).toBe(content);
529
+ expect(result.current.isVirtualized).toBe(false);
530
+ });
531
+
532
+ it('returns content as-is when below threshold', () => {
533
+ const content = 'a'.repeat(50000);
534
+ const { result } = renderHook(() => useVirtualization(content, 100000));
535
+
536
+ expect(result.current.content).toBe(content);
537
+ expect(result.current.isVirtualized).toBe(false);
538
+ });
539
+
540
+ it('truncates very large content', () => {
541
+ const content = 'a'.repeat(150000);
542
+ const { result } = renderHook(() => useVirtualization(content, 100000));
543
+
544
+ expect(result.current.isVirtualized).toBe(true);
545
+ expect(result.current.originalSize).toBe(150000);
546
+ expect(result.current.content.length).toBeLessThan(content.length);
547
+ expect(result.current.content).toContain('Content truncated for performance');
548
+ });
549
+
550
+ it('truncates to 50KB for large content', () => {
551
+ const content = 'a'.repeat(200000);
552
+ const { result } = renderHook(() => useVirtualization(content, 100000));
553
+
554
+ expect(result.current.isVirtualized).toBe(true);
555
+ expect(result.current.content).toContain('a'.repeat(50000));
556
+ });
557
+
558
+ it('handles custom threshold', () => {
559
+ const content = 'a'.repeat(60000);
560
+ const { result } = renderHook(() => useVirtualization(content, 50000));
561
+
562
+ expect(result.current.isVirtualized).toBe(true);
563
+ });
564
+
565
+ it('handles empty content', () => {
566
+ const { result } = renderHook(() => useVirtualization(''));
567
+
568
+ expect(result.current.content).toBe('');
569
+ expect(result.current.isVirtualized).toBe(false);
570
+ });
571
+
572
+ it('handles null content', () => {
573
+ const { result } = renderHook(() => useVirtualization(null));
574
+
575
+ expect(result.current.content).toBe(null);
576
+ expect(result.current.isVirtualized).toBe(false);
577
+ });
578
+
579
+ it('memoizes result', () => {
580
+ const content = 'a'.repeat(5000);
581
+ const { result, rerender } = renderHook(
582
+ ({ c }) => useVirtualization(c, 100000),
583
+ { initialProps: { c: content } }
584
+ );
585
+
586
+ const firstResult = result.current;
587
+
588
+ rerender({ c: content });
589
+
590
+ expect(result.current).toBe(firstResult);
591
+ });
592
+
593
+ it('updates when content changes', () => {
594
+ const content1 = 'a'.repeat(5000);
595
+ const content2 = 'b'.repeat(5000);
596
+ const { result, rerender } = renderHook(
597
+ ({ c }) => useVirtualization(c, 100000),
598
+ { initialProps: { c: content1 } }
599
+ );
600
+
601
+ const firstResult = result.current;
602
+
603
+ rerender({ c: content2 });
604
+
605
+ expect(result.current).not.toBe(firstResult);
606
+ expect(result.current.content).toBe(content2);
607
+ });
608
+ });
609
+
610
+ describe('PERFORMANCE_THRESHOLDS', () => {
611
+ it('exports correct threshold values', () => {
612
+ // Debounce thresholds
613
+ expect(PERFORMANCE_THRESHOLDS.SMALL_CONTENT_DEBOUNCE).toBe(1000);
614
+ expect(PERFORMANCE_THRESHOLDS.MEDIUM_CONTENT_DEBOUNCE).toBe(10000);
615
+ expect(PERFORMANCE_THRESHOLDS.LARGE_CONTENT_DEBOUNCE).toBe(50000);
616
+
617
+ // Analysis thresholds
618
+ expect(PERFORMANCE_THRESHOLDS.SMALL_CONTENT_ANALYSIS).toBe(10000);
619
+ expect(PERFORMANCE_THRESHOLDS.LARGE_CONTENT_ANALYSIS).toBe(50000);
620
+ expect(PERFORMANCE_THRESHOLDS.HUGE_CONTENT).toBe(100000);
621
+ });
622
+
623
+ it('exports correct debounce values', () => {
624
+ expect(PERFORMANCE_THRESHOLDS.DEBOUNCE_FAST).toBe(200);
625
+ expect(PERFORMANCE_THRESHOLDS.DEBOUNCE_NORMAL).toBe(300);
626
+ expect(PERFORMANCE_THRESHOLDS.DEBOUNCE_SLOW).toBe(600);
627
+ });
628
+
629
+ it('has consistent threshold ordering', () => {
630
+ // Debounce thresholds ordering
631
+ expect(PERFORMANCE_THRESHOLDS.SMALL_CONTENT_DEBOUNCE).toBeLessThan(
632
+ PERFORMANCE_THRESHOLDS.MEDIUM_CONTENT_DEBOUNCE
633
+ );
634
+ expect(PERFORMANCE_THRESHOLDS.MEDIUM_CONTENT_DEBOUNCE).toBeLessThan(
635
+ PERFORMANCE_THRESHOLDS.LARGE_CONTENT_DEBOUNCE
636
+ );
637
+
638
+ // Analysis thresholds ordering
639
+ expect(PERFORMANCE_THRESHOLDS.SMALL_CONTENT_ANALYSIS).toBeLessThan(
640
+ PERFORMANCE_THRESHOLDS.LARGE_CONTENT_ANALYSIS
641
+ );
642
+ expect(PERFORMANCE_THRESHOLDS.LARGE_CONTENT_ANALYSIS).toBeLessThan(
643
+ PERFORMANCE_THRESHOLDS.HUGE_CONTENT
644
+ );
645
+ });
646
+
647
+ it('has consistent debounce ordering', () => {
648
+ expect(PERFORMANCE_THRESHOLDS.DEBOUNCE_FAST).toBeLessThan(
649
+ PERFORMANCE_THRESHOLDS.DEBOUNCE_NORMAL
650
+ );
651
+ expect(PERFORMANCE_THRESHOLDS.DEBOUNCE_NORMAL).toBeLessThan(
652
+ PERFORMANCE_THRESHOLDS.DEBOUNCE_SLOW
653
+ );
654
+ });
655
+ });
656
+
657
+ describe('Integration Tests', () => {
658
+ it('smart debounce works with content analysis', () => {
659
+ const callback = jest.fn();
660
+ const content = 'a'.repeat(75000);
661
+ const analysis = analyzeContentSize(content);
662
+
663
+ const { result } = renderHook(() => useSmartDebounce(callback, 300));
664
+
665
+ act(() => {
666
+ result.current(content, analysis.size);
667
+ });
668
+
669
+ act(() => {
670
+ jest.advanceTimersByTime(analysis.recommendedDebounce);
671
+ });
672
+
673
+ expect(callback).toHaveBeenCalledWith(content);
674
+ });
675
+
676
+ it('virtualization and content analysis work together', () => {
677
+ const largeContent = 'a'.repeat(150000);
678
+ const analysis = analyzeContentSize(largeContent);
679
+
680
+ expect(analysis.isLarge).toBe(true);
681
+ expect(analysis.shouldOptimize).toBe(true);
682
+
683
+ const { result } = renderHook(() => useVirtualization(largeContent, 100000));
684
+
685
+ expect(result.current.isVirtualized).toBe(true);
686
+ expect(result.current.content.length).toBeLessThan(largeContent.length);
687
+ });
688
+
689
+ it('handles realistic editor scenarios', () => {
690
+ // Small email template
691
+ const smallEmail = '<p>Hello World</p>';
692
+ const smallAnalysis = analyzeContentSize(smallEmail);
693
+ expect(smallAnalysis.shouldOptimize).toBe(false);
694
+ expect(smallAnalysis.recommendedDebounce).toBe(200);
695
+
696
+ // Large email template with images
697
+ const largeEmail = '<div>' + 'a'.repeat(60000) + '</div>';
698
+ const largeAnalysis = analyzeContentSize(largeEmail);
699
+ expect(largeAnalysis.shouldOptimize).toBe(true);
700
+ expect(largeAnalysis.recommendedDebounce).toBe(600);
701
+ });
702
+ });
703
+
704
+ describe('Edge Cases', () => {
705
+ it('handles rapid successive calls to smart debounce', () => {
706
+ const callback = jest.fn();
707
+ const { result } = renderHook(() => useSmartDebounce(callback, 300));
708
+
709
+ act(() => {
710
+ for (let i = 0; i < 10; i++) {
711
+ result.current(`value${i}`, 0); // Zero content size = small content delay (200ms)
712
+ }
713
+ });
714
+
715
+ act(() => {
716
+ jest.advanceTimersByTime(200);
717
+ });
718
+
719
+ expect(callback).toHaveBeenCalledTimes(1);
720
+ expect(callback).toHaveBeenCalledWith('value9');
721
+ });
722
+
723
+ it('handles content at exact threshold boundaries', () => {
724
+ const exactly10KB = analyzeContentSize('a'.repeat(10000));
725
+ expect(exactly10KB.isSmall).toBe(false);
726
+ expect(exactly10KB.isMedium).toBe(true);
727
+
728
+ const exactly50KB = analyzeContentSize('a'.repeat(50000));
729
+ expect(exactly50KB.isMedium).toBe(false);
730
+ expect(exactly50KB.isLarge).toBe(true);
731
+
732
+ // Exactly at threshold (100000) should be virtualized (>= threshold)
733
+ const exactly100KB = 'a'.repeat(100000);
734
+ const { result } = renderHook(() => useVirtualization(exactly100KB, 100000));
735
+ expect(result.current.isVirtualized).toBe(true);
736
+
737
+ // Just below threshold should NOT be virtualized
738
+ const justBelow100KB = 'a'.repeat(99999);
739
+ const { result: result2 } = renderHook(() => useVirtualization(justBelow100KB, 100000));
740
+ expect(result2.current.isVirtualized).toBe(false);
741
+ });
742
+
743
+ it('handles very small debounce delays', () => {
744
+ const callback = jest.fn();
745
+ const { result } = renderHook(() => useSmartDebounce(callback, 1));
746
+
747
+ act(() => {
748
+ result.current('test', 100);
749
+ });
750
+
751
+ act(() => {
752
+ jest.advanceTimersByTime(200);
753
+ });
754
+
755
+ expect(callback).toHaveBeenCalled();
756
+ });
757
+ });
758
+
759
+ describe('Performance Benchmarking', () => {
760
+ it('measures smart debounce performance', () => {
761
+ const callback = jest.fn();
762
+ const startTime = performance.now();
763
+
764
+ const { result } = renderHook(() => useSmartDebounce(callback, 300));
765
+
766
+ act(() => {
767
+ for (let i = 0; i < 1000; i++) {
768
+ result.current(`test${i}`, 1000);
769
+ }
770
+ });
771
+
772
+ const endTime = performance.now();
773
+ const duration = endTime - startTime;
774
+
775
+ // Should complete quickly (< 100ms for 1000 calls)
776
+ expect(duration).toBeLessThan(100);
777
+ });
778
+
779
+ it('measures content analysis performance', () => {
780
+ const startTime = performance.now();
781
+
782
+ for (let i = 0; i < 100; i++) {
783
+ const content = 'a'.repeat(i * 1000);
784
+ analyzeContentSize(content);
785
+ }
786
+
787
+ const endTime = performance.now();
788
+ const duration = endTime - startTime;
789
+
790
+ // Should complete quickly (< 50ms for 100 analyses)
791
+ expect(duration).toBeLessThan(50);
792
+ });
793
+
794
+ it('measures virtualization performance with large content', () => {
795
+ const largeContent = 'a'.repeat(500000); // 500KB
796
+ const startTime = performance.now();
797
+
798
+ const { result } = renderHook(() => useVirtualization(largeContent, 100000));
799
+
800
+ const endTime = performance.now();
801
+ const duration = endTime - startTime;
802
+
803
+ expect(result.current.isVirtualized).toBe(true);
804
+ expect(duration).toBeLessThan(50); // Should be fast even for large content
805
+ });
806
+ });
807
+
808
+ describe('Memory Management', () => {
809
+ it('properly cleans up debounce timeouts', () => {
810
+ const callback = jest.fn();
811
+ const { result, unmount } = renderHook(() => useSmartDebounce(callback, 300));
812
+
813
+ act(() => {
814
+ result.current('test');
815
+ });
816
+
817
+ // Unmount before timeout completes
818
+ unmount();
819
+
820
+ act(() => {
821
+ jest.advanceTimersByTime(300);
822
+ });
823
+
824
+ // After unmount and timer advance, callback should NOT be called
825
+ // The cleanup effect should have cleared the pending timeout
826
+ expect(callback).not.toHaveBeenCalled();
827
+ });
828
+
829
+ it('handles multiple cleanup functions', () => {
830
+ const cleanup1 = jest.fn();
831
+ const cleanup2 = jest.fn();
832
+
833
+ const { unmount: unmount1 } = renderHook(() => useCleanup(cleanup1));
834
+ const { unmount: unmount2 } = renderHook(() => useCleanup(cleanup2));
835
+
836
+ unmount1();
837
+ expect(cleanup1).toHaveBeenCalledTimes(1);
838
+ expect(cleanup2).not.toHaveBeenCalled();
839
+
840
+ unmount2();
841
+ expect(cleanup2).toHaveBeenCalledTimes(1);
842
+ });
843
+
844
+ it('prevents memory leaks with large content virtualization', () => {
845
+ const contents = [];
846
+
847
+ // Create multiple large content instances
848
+ for (let i = 0; i < 10; i++) {
849
+ contents.push('a'.repeat(200000)); // 200KB each
850
+ }
851
+
852
+ const { result, rerender } = renderHook(
853
+ ({ content }) => useVirtualization(content, 100000),
854
+ { initialProps: { content: contents[0] } }
855
+ );
856
+
857
+ // Switch between different large contents
858
+ contents.forEach(content => {
859
+ rerender({ content });
860
+ expect(result.current.isVirtualized).toBe(true);
861
+ expect(result.current.content.length).toBeLessThan(content.length);
862
+ });
863
+ });
864
+ });
865
+
866
+ describe('Real-world Scenarios', () => {
867
+ it('handles typical HTML email content', () => {
868
+ const htmlEmail = `
869
+ <html>
870
+ <body>
871
+ <div style="font-family: Arial, sans-serif;">
872
+ <h1>Welcome to our newsletter!</h1>
873
+ <p>This is a typical email with some content.</p>
874
+ <img src="https://example.com/image.jpg" alt="Example" />
875
+ <table>
876
+ <tr><td>Cell 1</td><td>Cell 2</td></tr>
877
+ </table>
878
+ </div>
879
+ </body>
880
+ </html>
881
+ `;
882
+
883
+ const analysis = analyzeContentSize(htmlEmail);
884
+ expect(analysis.shouldOptimize).toBe(false); // Small email
885
+ expect(analysis.recommendedDebounce).toBe(200);
886
+
887
+ const { result } = renderHook(() => useVirtualization(htmlEmail));
888
+ expect(result.current.isVirtualized).toBe(false);
889
+ });
890
+
891
+ it('handles large marketing email with inline styles', () => {
892
+ const largeEmail = `
893
+ <html>
894
+ <head><style>/* Large CSS block */</style></head>
895
+ <body>
896
+ ${'<div style="margin: 10px; padding: 20px;">'.repeat(2000)}
897
+ Large email content with many nested divs
898
+ ${'</div>'.repeat(2000)}
899
+ </body>
900
+ </html>
901
+ `;
902
+
903
+ const analysis = analyzeContentSize(largeEmail);
904
+ expect(analysis.shouldOptimize).toBe(true);
905
+ expect(analysis.isLarge).toBe(true);
906
+
907
+ const { result } = renderHook(() => useVirtualization(largeEmail, 50000));
908
+ expect(result.current.isVirtualized).toBe(true);
909
+ });
910
+
911
+ it('optimizes editor performance for different content types', () => {
912
+ const scenarios = [
913
+ { name: 'Plain text', content: 'Hello world!', expectOptimize: false },
914
+ { name: 'Small HTML', content: '<p>Hello <b>world</b>!</p>', expectOptimize: false },
915
+ { name: 'Medium email', content: '<div>' + 'a'.repeat(15000) + '</div>', expectOptimize: true },
916
+ { name: 'Large template', content: '<html>' + 'b'.repeat(60000) + '</html>', expectOptimize: true }
917
+ ];
918
+
919
+ scenarios.forEach(scenario => {
920
+ const analysis = analyzeContentSize(scenario.content);
921
+ expect(analysis.shouldOptimize).toBe(scenario.expectOptimize);
922
+
923
+ if (scenario.expectOptimize) {
924
+ expect(analysis.recommendedDebounce).toBeGreaterThan(200);
925
+ } else {
926
+ expect(analysis.recommendedDebounce).toBe(200);
927
+ }
928
+ });
929
+ });
930
+
931
+ it('integrates all performance optimizations', () => {
932
+ const TestComponent = ({ content }) => <div>{content}</div>;
933
+ const OptimizedComponent = withPerformanceOptimization(TestComponent);
934
+
935
+ const largeContent = 'a'.repeat(75000);
936
+ const analysis = analyzeContentSize(largeContent);
937
+
938
+ // Test virtualization within a hook context
939
+ const { result } = renderHook(() => useVirtualization(largeContent, 50000));
940
+ const virtualization = result.current;
941
+
942
+ // Should recommend optimizations for large content
943
+ expect(analysis.shouldOptimize).toBe(true);
944
+ expect(analysis.recommendedDebounce).toBe(600);
945
+ expect(virtualization.isVirtualized).toBe(true);
946
+
947
+ // Component should render with optimized content
948
+ const { rerender } = render(
949
+ <OptimizedComponent content={virtualization.content} />
950
+ );
951
+
952
+ // Should not re-render with same content
953
+ rerender(<OptimizedComponent content={virtualization.content} />);
954
+ });
955
+ });
956
+
957
+ describe('Cross-browser Compatibility', () => {
958
+ it('works with different timer implementations', () => {
959
+ const originalSetTimeout = global.setTimeout;
960
+ const originalClearTimeout = global.clearTimeout;
961
+
962
+ // Mock different timer behavior
963
+ const mockSetTimeout = jest.fn((fn, delay) => originalSetTimeout(fn, delay));
964
+ const mockClearTimeout = jest.fn(originalClearTimeout);
965
+
966
+ global.setTimeout = mockSetTimeout;
967
+ global.clearTimeout = mockClearTimeout;
968
+
969
+ const callback = jest.fn();
970
+ const { result } = renderHook(() => useSmartDebounce(callback, 300));
971
+
972
+ act(() => {
973
+ result.current('test');
974
+ });
975
+
976
+ expect(mockSetTimeout).toHaveBeenCalled();
977
+
978
+ act(() => {
979
+ result.current('test2');
980
+ });
981
+
982
+ expect(mockClearTimeout).toHaveBeenCalled();
983
+
984
+ // Restore original functions
985
+ global.setTimeout = originalSetTimeout;
986
+ global.clearTimeout = originalClearTimeout;
987
+ });
988
+
989
+ it('handles performance.now() availability', () => {
990
+ const originalPerformance = global.performance;
991
+
992
+ // Test without performance.now()
993
+ global.performance = undefined;
994
+
995
+ expect(() => {
996
+ analyzeContentSize('test content');
997
+ }).not.toThrow();
998
+
999
+ // Restore
1000
+ global.performance = originalPerformance;
1001
+ });
1002
+
1003
+ it('works with different React versions', () => {
1004
+ // Test that hooks work correctly
1005
+ const { result } = renderHook(() => {
1006
+ const debounce = useSmartDebounce(() => {}, 300);
1007
+ const cleanup = useCleanup(() => {});
1008
+ const virtualization = useVirtualization('test');
1009
+
1010
+ return { debounce, cleanup, virtualization };
1011
+ });
1012
+
1013
+ expect(result.current.debounce).toBeInstanceOf(Function);
1014
+ expect(result.current.virtualization).toHaveProperty('content');
1015
+ expect(result.current.virtualization).toHaveProperty('isVirtualized');
1016
+ });
1017
+ });
1018
+
1019
+ describe('Error Resilience', () => {
1020
+ it('handles callback errors in debounce', () => {
1021
+ const errorCallback = jest.fn(() => {
1022
+ throw new Error('Callback error');
1023
+ });
1024
+
1025
+ const { result } = renderHook(() => useSmartDebounce(errorCallback, 300));
1026
+
1027
+ act(() => {
1028
+ result.current('test');
1029
+ });
1030
+
1031
+ expect(() => {
1032
+ act(() => {
1033
+ jest.advanceTimersByTime(300);
1034
+ });
1035
+ }).toThrow('Callback error');
1036
+
1037
+ expect(errorCallback).toHaveBeenCalled();
1038
+ });
1039
+
1040
+ it('handles cleanup function errors', () => {
1041
+ const errorCleanup = jest.fn(() => {
1042
+ throw new Error('Cleanup error');
1043
+ });
1044
+
1045
+ // Test that the cleanup function is properly set up
1046
+ const { unmount } = renderHook(() => useCleanup(errorCleanup));
1047
+
1048
+ // The cleanup function should be called on unmount
1049
+ // Note: React's error handling might suppress the error in test environment
1050
+ try {
1051
+ unmount();
1052
+ } catch (error) {
1053
+ expect(error.message).toBe('Cleanup error');
1054
+ }
1055
+
1056
+ expect(errorCleanup).toHaveBeenCalled();
1057
+ });
1058
+
1059
+ it('handles extreme content sizes gracefully', () => {
1060
+ // Very large content (10MB)
1061
+ const extremeContent = 'a'.repeat(10000000);
1062
+
1063
+ expect(() => {
1064
+ const analysis = analyzeContentSize(extremeContent);
1065
+ expect(analysis.isLarge).toBe(true);
1066
+ }).not.toThrow();
1067
+
1068
+ expect(() => {
1069
+ const { result } = renderHook(() => useVirtualization(extremeContent, 100000));
1070
+ expect(result.current.isVirtualized).toBe(true);
1071
+ }).not.toThrow();
1072
+ });
1073
+
1074
+ it('handles invalid content types gracefully', () => {
1075
+ const invalidInputs = [
1076
+ null,
1077
+ undefined,
1078
+ 123,
1079
+ {},
1080
+ [],
1081
+ true
1082
+ ];
1083
+
1084
+ invalidInputs.forEach(input => {
1085
+ expect(() => {
1086
+ analyzeContentSize(input);
1087
+ }).not.toThrow();
1088
+
1089
+ expect(() => {
1090
+ const { result } = renderHook(() => useVirtualization(input));
1091
+ expect(result.current).toBeDefined();
1092
+ }).not.toThrow();
1093
+ });
1094
+
1095
+ // Test Symbol separately as it might behave differently
1096
+ if (typeof Symbol !== 'undefined') {
1097
+ expect(() => {
1098
+ analyzeContentSize(Symbol('test'));
1099
+ }).not.toThrow();
1100
+
1101
+ expect(() => {
1102
+ const { result } = renderHook(() => useVirtualization(Symbol('test')));
1103
+ expect(result.current).toBeDefined();
1104
+ }).not.toThrow();
1105
+ }
1106
+ });
1107
+ });
1108
+ });
1109
+