@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.
- package/assets/Android.png +0 -0
- package/assets/iOS.png +0 -0
- package/config/app.js +1 -2
- package/package.json +16 -2
- package/services/api.js +0 -2
- package/v2Components/HtmlEditor/HTMLEditor.js +508 -0
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1809 -0
- package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +532 -0
- package/v2Components/HtmlEditor/_htmlEditor.scss +304 -0
- package/v2Components/HtmlEditor/_index.lazy.scss +26 -0
- package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +376 -0
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +331 -0
- package/v2Components/HtmlEditor/components/DeviceToggle/__tests__/index.test.js +314 -0
- package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +244 -0
- package/v2Components/HtmlEditor/components/DeviceToggle/index.js +111 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/PreviewModeGroup.js +72 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/__tests__/PreviewModeGroup.test.js +1594 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +113 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/_previewModeGroup.scss +82 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/index.js +115 -0
- package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +57 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/ContentOverlay.js +90 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +60 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/LayoutSelector.js +58 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/ContentOverlay.test.js +389 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +424 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/LayoutSelector.test.js +248 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +253 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +104 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +179 -0
- package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +220 -0
- package/v2Components/HtmlEditor/components/PreviewPane/index.js +229 -0
- package/v2Components/HtmlEditor/components/SplitContainer/SplitContainer.js +276 -0
- package/v2Components/HtmlEditor/components/SplitContainer/__tests__/SplitContainer.test.js +295 -0
- package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +257 -0
- package/v2Components/HtmlEditor/components/SplitContainer/index.js +7 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +152 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +31 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +70 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/__tests__/index.test.js +98 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +311 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/index.js +297 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/messages.js +57 -0
- package/v2Components/HtmlEditor/components/common/EditorContext.js +84 -0
- package/v2Components/HtmlEditor/components/common/__tests__/EditorContext.test.js +660 -0
- package/v2Components/HtmlEditor/constants.js +241 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useEditorContent.test.js +450 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +785 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useLayoutState.test.js +580 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useValidation.enhanced.test.js +768 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +590 -0
- package/v2Components/HtmlEditor/hooks/useEditorContent.js +274 -0
- package/v2Components/HtmlEditor/hooks/useInAppContent.js +407 -0
- package/v2Components/HtmlEditor/hooks/useLayoutState.js +247 -0
- package/v2Components/HtmlEditor/hooks/useValidation.js +325 -0
- package/v2Components/HtmlEditor/index.js +29 -0
- package/v2Components/HtmlEditor/index.lazy.js +114 -0
- package/v2Components/HtmlEditor/messages.js +389 -0
- package/v2Components/HtmlEditor/utils/__tests__/contentSanitizer.test.js +741 -0
- package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +1042 -0
- package/v2Components/HtmlEditor/utils/__tests__/liquidTemplateSupport.test.js +515 -0
- package/v2Components/HtmlEditor/utils/__tests__/properSyntaxHighlighting.test.js +473 -0
- package/v2Components/HtmlEditor/utils/__tests__/simplePerformance.test.js +1109 -0
- package/v2Components/HtmlEditor/utils/__tests__/validationAdapter.test.js +240 -0
- package/v2Components/HtmlEditor/utils/contentSanitizer.js +433 -0
- package/v2Components/HtmlEditor/utils/htmlValidator.js +508 -0
- package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +524 -0
- package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +163 -0
- package/v2Components/HtmlEditor/utils/simplePerformance.js +145 -0
- package/v2Components/HtmlEditor/utils/validationAdapter.js +130 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +0 -2
- package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +200 -0
- package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +545 -0
- package/v2Containers/EmailWrapper/index.js +8 -1
- package/v2Containers/Templates/constants.js +8 -0
- package/v2Containers/Templates/index.js +56 -28
- 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
|
+
|