@capillarytech/creatives-library 8.0.208 → 8.0.210

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 (76) 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 +403 -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/Rcs/index.js +2 -0
  73. package/v2Containers/Templates/constants.js +8 -0
  74. package/v2Containers/Templates/index.js +56 -28
  75. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +5 -14
  76. package/v2Containers/Whatsapp/index.js +1 -0
@@ -0,0 +1,768 @@
1
+ /**
2
+ * Enhanced useValidation Hook Tests
3
+ *
4
+ * Additional comprehensive tests for the useValidation custom hook
5
+ * These tests focus on hook behavior, edge cases, and integration scenarios
6
+ */
7
+
8
+ import React from 'react';
9
+ import { render, screen, act, waitFor } from '@testing-library/react';
10
+ import '@testing-library/jest-dom';
11
+ import { useValidation } from '../useValidation';
12
+
13
+ // Test wrapper component
14
+ const TestComponent = ({ content, variant, options, onStateChange }) => {
15
+ const validationState = useValidation(content, variant, options);
16
+
17
+ // Effect to notify parent of validation state changes
18
+ // Note: In production, onStateChange should be wrapped with useCallback
19
+ // to avoid unnecessary re-runs. In tests, inline functions are acceptable.
20
+ React.useEffect(() => {
21
+ if (onStateChange) {
22
+ onStateChange(validationState);
23
+ }
24
+ }, [validationState, onStateChange]);
25
+
26
+ return (
27
+ <div>
28
+ <div data-testid="is-validating">{String(validationState.isValidating)}</div>
29
+ <div data-testid="is-valid">{String(validationState.isValid)}</div>
30
+ <div data-testid="is-secure">{String(validationState.isSecure)}</div>
31
+ <div data-testid="is-clean">{String(validationState.isClean())}</div>
32
+ <div data-testid="has-errors">{String(validationState.hasErrors)}</div>
33
+ <div data-testid="has-warnings">{String(validationState.hasWarnings)}</div>
34
+ <div data-testid="has-security-issues">{String(validationState.hasSecurityIssues)}</div>
35
+ <div data-testid="total-errors">{validationState.summary.totalErrors}</div>
36
+ <div data-testid="total-warnings">{validationState.summary.totalWarnings}</div>
37
+ <div data-testid="total-info">{validationState.summary.totalInfo}</div>
38
+ <div data-testid="html-errors-count">{validationState.htmlErrors.length}</div>
39
+ <div data-testid="css-errors-count">{validationState.cssErrors.length}</div>
40
+ <div data-testid="security-issues-count">{validationState.securityIssues.length}</div>
41
+ <div data-testid="sanitization-warnings-count">{validationState.sanitizationWarnings.length}</div>
42
+ <div data-testid="last-validated">{validationState.lastValidated ? 'validated' : 'not-validated'}</div>
43
+ </div>
44
+ );
45
+ };
46
+
47
+ describe('Enhanced useValidation Hook Tests', () => {
48
+ // Use fake timers for debouncing tests
49
+ beforeEach(() => {
50
+ jest.useFakeTimers();
51
+ jest.clearAllMocks();
52
+ });
53
+
54
+ afterEach(() => {
55
+ jest.runOnlyPendingTimers();
56
+ jest.useRealTimers();
57
+ });
58
+
59
+ describe('Hook Structure and API', () => {
60
+ it('returns all expected properties and methods', async () => {
61
+ let validationState;
62
+
63
+ render(<TestComponent
64
+ content="<div>test</div>"
65
+ onStateChange={(state) => { validationState = state; }}
66
+ />);
67
+
68
+ await act(async () => {
69
+ jest.advanceTimersByTime(500);
70
+ await Promise.resolve();
71
+ });
72
+
73
+ // Check all expected properties exist
74
+ expect(validationState).toHaveProperty('isValidating');
75
+ expect(validationState).toHaveProperty('lastValidated');
76
+ expect(validationState).toHaveProperty('htmlErrors');
77
+ expect(validationState).toHaveProperty('htmlWarnings');
78
+ expect(validationState).toHaveProperty('htmlInfo');
79
+ expect(validationState).toHaveProperty('cssErrors');
80
+ expect(validationState).toHaveProperty('cssWarnings');
81
+ expect(validationState).toHaveProperty('cssInfo');
82
+ expect(validationState).toHaveProperty('securityIssues');
83
+ expect(validationState).toHaveProperty('sanitizationWarnings');
84
+ expect(validationState).toHaveProperty('isValid');
85
+ expect(validationState).toHaveProperty('isSecure');
86
+ expect(validationState).toHaveProperty('summary');
87
+
88
+ // Check all expected methods exist
89
+ expect(typeof validationState.validateContent).toBe('function');
90
+ expect(typeof validationState.forceValidation).toBe('function');
91
+ expect(typeof validationState.clearValidation).toBe('function');
92
+ expect(typeof validationState.getErrorsBySeverity).toBe('function');
93
+ expect(typeof validationState.getErrorsBySource).toBe('function');
94
+ expect(typeof validationState.getAllIssues).toBe('function');
95
+ expect(typeof validationState.isClean).toBe('function');
96
+
97
+ // Check computed properties
98
+ expect(validationState).toHaveProperty('hasErrors');
99
+ expect(validationState).toHaveProperty('hasWarnings');
100
+ expect(validationState).toHaveProperty('hasSecurityIssues');
101
+ });
102
+
103
+ it('has correct initial state structure', () => {
104
+ let validationState;
105
+
106
+ render(<TestComponent
107
+ content=""
108
+ onStateChange={(state) => { validationState = state; }}
109
+ />);
110
+
111
+ expect(validationState.isValidating).toBe(false);
112
+ expect(validationState.lastValidated).toBeNull();
113
+ expect(Array.isArray(validationState.htmlErrors)).toBe(true);
114
+ expect(Array.isArray(validationState.htmlWarnings)).toBe(true);
115
+ expect(Array.isArray(validationState.htmlInfo)).toBe(true);
116
+ expect(Array.isArray(validationState.cssErrors)).toBe(true);
117
+ expect(Array.isArray(validationState.cssWarnings)).toBe(true);
118
+ expect(Array.isArray(validationState.cssInfo)).toBe(true);
119
+ expect(Array.isArray(validationState.securityIssues)).toBe(true);
120
+ expect(Array.isArray(validationState.sanitizationWarnings)).toBe(true);
121
+ expect(typeof validationState.isValid).toBe('boolean');
122
+ expect(typeof validationState.isSecure).toBe('boolean');
123
+ expect(typeof validationState.summary).toBe('object');
124
+ });
125
+ });
126
+
127
+ describe('Advanced Configuration Options', () => {
128
+ describe('Debouncing Behavior', () => {
129
+ it('respects custom debounce timing', async () => {
130
+ const options = { debounceMs: 1000 };
131
+
132
+ render(<TestComponent content="<div>test</div>" options={options} />);
133
+
134
+ // Should not validate immediately
135
+ await act(async () => {
136
+ jest.advanceTimersByTime(500);
137
+ await Promise.resolve();
138
+ });
139
+ expect(screen.getByTestId('last-validated')).toHaveTextContent('not-validated');
140
+
141
+ // Should validate after full debounce period
142
+ await act(async () => {
143
+ jest.advanceTimersByTime(500);
144
+ await Promise.resolve();
145
+ });
146
+ expect(screen.getByTestId('last-validated')).toHaveTextContent('validated');
147
+ });
148
+
149
+ it('handles zero debounce correctly', async () => {
150
+ const options = { debounceMs: 0 };
151
+
152
+ render(<TestComponent content="<div>test</div>" options={options} />);
153
+
154
+ await act(async () => {
155
+ jest.advanceTimersByTime(0);
156
+ await Promise.resolve();
157
+ });
158
+
159
+ expect(screen.getByTestId('last-validated')).toHaveTextContent('validated');
160
+ });
161
+
162
+ it('handles very long debounce periods', async () => {
163
+ const options = { debounceMs: 5000 };
164
+
165
+ render(<TestComponent content="<div>test</div>" options={options} />);
166
+
167
+ await act(async () => {
168
+ jest.advanceTimersByTime(4999);
169
+ await Promise.resolve();
170
+ });
171
+ expect(screen.getByTestId('last-validated')).toHaveTextContent('not-validated');
172
+
173
+ await act(async () => {
174
+ jest.advanceTimersByTime(1);
175
+ await Promise.resolve();
176
+ });
177
+ expect(screen.getByTestId('last-validated')).toHaveTextContent('validated');
178
+ });
179
+ });
180
+
181
+ describe('Real-time Validation Control', () => {
182
+ it('disables real-time validation when configured', async () => {
183
+ const options = { enableRealTime: false };
184
+
185
+ render(<TestComponent content="<div>test</div>" options={options} />);
186
+
187
+ await act(async () => {
188
+ jest.advanceTimersByTime(1000);
189
+ await Promise.resolve();
190
+ });
191
+
192
+ expect(screen.getByTestId('last-validated')).toHaveTextContent('not-validated');
193
+ });
194
+
195
+ it('enables real-time validation by default', async () => {
196
+ render(<TestComponent content="<div>test</div>" />);
197
+
198
+ await act(async () => {
199
+ jest.advanceTimersByTime(500);
200
+ await Promise.resolve();
201
+ });
202
+
203
+ expect(screen.getByTestId('last-validated')).toHaveTextContent('validated');
204
+ });
205
+ });
206
+
207
+ describe('Sanitization Configuration', () => {
208
+ it('enables sanitization by default', async () => {
209
+ let validationState;
210
+
211
+ render(<TestComponent
212
+ content="<div>test</div>"
213
+ onStateChange={(state) => { validationState = state; }}
214
+ />);
215
+
216
+ await act(async () => {
217
+ jest.advanceTimersByTime(500);
218
+ await Promise.resolve();
219
+ });
220
+
221
+ // Should have called sanitization (verified by checking the state structure)
222
+ expect(Array.isArray(validationState.sanitizationWarnings)).toBe(true);
223
+ });
224
+
225
+ it('disables sanitization when configured', async () => {
226
+ const options = { enableSanitization: false };
227
+
228
+ render(<TestComponent content="<div>test</div>" options={options} />);
229
+
230
+ await act(async () => {
231
+ jest.advanceTimersByTime(500);
232
+ await Promise.resolve();
233
+ });
234
+
235
+ expect(screen.getByTestId('sanitization-warnings-count')).toHaveTextContent('0');
236
+ });
237
+
238
+ it('handles different security levels', async () => {
239
+ const options = { securityLevel: 'strict' };
240
+
241
+ render(<TestComponent content="<div>test</div>" options={options} />);
242
+
243
+ await act(async () => {
244
+ jest.advanceTimersByTime(500);
245
+ await Promise.resolve();
246
+ });
247
+
248
+ expect(screen.getByTestId('is-validating')).toHaveTextContent('false');
249
+ });
250
+ });
251
+ });
252
+
253
+ describe('Variant Handling', () => {
254
+ it('handles email variant', async () => {
255
+ render(<TestComponent content="<div>test</div>" variant="email" />);
256
+
257
+ await act(async () => {
258
+ jest.advanceTimersByTime(500);
259
+ await Promise.resolve();
260
+ });
261
+
262
+ expect(screen.getByTestId('last-validated')).toHaveTextContent('validated');
263
+ });
264
+
265
+ it('handles inapp variant', async () => {
266
+ render(<TestComponent content="<div>test</div>" variant="inapp" />);
267
+
268
+ await act(async () => {
269
+ jest.advanceTimersByTime(500);
270
+ await Promise.resolve();
271
+ });
272
+
273
+ expect(screen.getByTestId('last-validated')).toHaveTextContent('validated');
274
+ });
275
+
276
+ it('handles unknown variant gracefully', async () => {
277
+ render(<TestComponent content="<div>test</div>" variant="unknown" />);
278
+
279
+ await act(async () => {
280
+ jest.advanceTimersByTime(500);
281
+ await Promise.resolve();
282
+ });
283
+
284
+ expect(screen.getByTestId('last-validated')).toHaveTextContent('validated');
285
+ });
286
+
287
+ it('switches between variants correctly', async () => {
288
+ const { rerender } = render(<TestComponent content="<div>test</div>" variant="email" />);
289
+
290
+ await act(async () => {
291
+ jest.advanceTimersByTime(500);
292
+ await Promise.resolve();
293
+ });
294
+
295
+ expect(screen.getByTestId('last-validated')).toHaveTextContent('validated');
296
+
297
+ rerender(<TestComponent content="<div>test</div>" variant="inapp" />);
298
+
299
+ await act(async () => {
300
+ jest.advanceTimersByTime(500);
301
+ await Promise.resolve();
302
+ });
303
+
304
+ expect(screen.getByTestId('last-validated')).toHaveTextContent('validated');
305
+ });
306
+ });
307
+
308
+ describe('Method Functionality', () => {
309
+ describe('forceValidation', () => {
310
+ it('bypasses debouncing', async () => {
311
+ let validationState;
312
+ const options = { debounceMs: 5000 };
313
+
314
+ render(<TestComponent
315
+ content="<div>test</div>"
316
+ options={options}
317
+ onStateChange={(state) => { validationState = state; }}
318
+ />);
319
+
320
+ // Should not validate yet due to long debounce
321
+ await act(async () => {
322
+ jest.advanceTimersByTime(1000);
323
+ await Promise.resolve();
324
+ });
325
+ expect(screen.getByTestId('last-validated')).toHaveTextContent('not-validated');
326
+
327
+ // Force validation should bypass debounce
328
+ await act(async () => {
329
+ validationState.forceValidation();
330
+ await Promise.resolve();
331
+ });
332
+ expect(screen.getByTestId('last-validated')).toHaveTextContent('validated');
333
+ });
334
+
335
+ it('validates immediately', async () => {
336
+ let validationState;
337
+
338
+ render(<TestComponent
339
+ content="<div>test</div>"
340
+ onStateChange={(state) => { validationState = state; }}
341
+ />);
342
+
343
+ await act(async () => {
344
+ validationState.forceValidation();
345
+ await Promise.resolve();
346
+ });
347
+
348
+ expect(screen.getByTestId('last-validated')).toHaveTextContent('validated');
349
+ });
350
+ });
351
+
352
+ describe('clearValidation', () => {
353
+ it('resets validation state', async () => {
354
+ let validationState;
355
+
356
+ render(<TestComponent
357
+ content="<div>test</div>"
358
+ onStateChange={(state) => { validationState = state; }}
359
+ />);
360
+
361
+ // Wait for validation
362
+ await act(async () => {
363
+ jest.advanceTimersByTime(500);
364
+ await Promise.resolve();
365
+ });
366
+
367
+ expect(screen.getByTestId('last-validated')).toHaveTextContent('validated');
368
+
369
+ // Clear validation
370
+ await act(async () => {
371
+ validationState.clearValidation();
372
+ await Promise.resolve();
373
+ });
374
+
375
+ expect(screen.getByTestId('last-validated')).toHaveTextContent('not-validated');
376
+ expect(screen.getByTestId('is-valid')).toHaveTextContent('true');
377
+ expect(screen.getByTestId('is-secure')).toHaveTextContent('true');
378
+ expect(screen.getByTestId('total-errors')).toHaveTextContent('0');
379
+ expect(screen.getByTestId('total-warnings')).toHaveTextContent('0');
380
+ });
381
+
382
+ it('cancels pending validation', async () => {
383
+ let validationState;
384
+ const options = { debounceMs: 2000 };
385
+
386
+ render(<TestComponent
387
+ content="<div>test</div>"
388
+ options={options}
389
+ onStateChange={(state) => { validationState = state; }}
390
+ />);
391
+
392
+ // Start validation but don't wait for completion
393
+ await act(async () => {
394
+ jest.advanceTimersByTime(1000);
395
+ await Promise.resolve();
396
+ });
397
+
398
+ // Clear validation before debounce completes
399
+ await act(async () => {
400
+ validationState.clearValidation();
401
+ await Promise.resolve();
402
+ });
403
+
404
+ // Advance past original debounce time
405
+ await act(async () => {
406
+ jest.advanceTimersByTime(2000);
407
+ await Promise.resolve();
408
+ });
409
+
410
+ // Should still be not validated
411
+ expect(screen.getByTestId('last-validated')).toHaveTextContent('not-validated');
412
+ });
413
+ });
414
+
415
+ describe('validateContent', () => {
416
+ it('validates new content with debouncing', async () => {
417
+ let validationState;
418
+
419
+ render(<TestComponent
420
+ content="<div>initial</div>"
421
+ onStateChange={(state) => { validationState = state; }}
422
+ />);
423
+
424
+ await act(async () => {
425
+ validationState.validateContent('<div>new content</div>');
426
+ jest.advanceTimersByTime(500);
427
+ await Promise.resolve();
428
+ });
429
+
430
+ expect(screen.getByTestId('last-validated')).toHaveTextContent('validated');
431
+ });
432
+
433
+ it('skips validation for unchanged content', async () => {
434
+ let validationState;
435
+ const content = '<div>test content</div>';
436
+
437
+ render(<TestComponent
438
+ content={content}
439
+ onStateChange={(state) => { validationState = state; }}
440
+ />);
441
+
442
+ // Initial validation
443
+ await act(async () => {
444
+ jest.advanceTimersByTime(500);
445
+ await Promise.resolve();
446
+ });
447
+
448
+ expect(screen.getByTestId('last-validated')).toHaveTextContent('validated');
449
+
450
+ // Clear the validated state to test skipping
451
+ await act(async () => {
452
+ validationState.clearValidation();
453
+ await Promise.resolve();
454
+ });
455
+
456
+ // Validate same content again - should skip because content hasn't changed
457
+ await act(async () => {
458
+ validationState.validateContent(content);
459
+ jest.advanceTimersByTime(500);
460
+ await Promise.resolve();
461
+ });
462
+
463
+ // Should remain not-validated because content didn't change
464
+ expect(screen.getByTestId('last-validated')).toHaveTextContent('not-validated');
465
+ });
466
+ });
467
+ });
468
+
469
+ describe('Utility Methods', () => {
470
+ describe('getErrorsBySeverity', () => {
471
+ it('returns function that filters by severity', async () => {
472
+ let validationState;
473
+
474
+ render(<TestComponent
475
+ content="<div>test</div>"
476
+ onStateChange={(state) => { validationState = state; }}
477
+ />);
478
+
479
+ await act(async () => {
480
+ jest.advanceTimersByTime(500);
481
+ await Promise.resolve();
482
+ });
483
+
484
+ const errors = validationState.getErrorsBySeverity('error');
485
+ const warnings = validationState.getErrorsBySeverity('warning');
486
+ const info = validationState.getErrorsBySeverity('info');
487
+
488
+ expect(Array.isArray(errors)).toBe(true);
489
+ expect(Array.isArray(warnings)).toBe(true);
490
+ expect(Array.isArray(info)).toBe(true);
491
+ });
492
+
493
+ it('returns empty array for non-existent severity', async () => {
494
+ let validationState;
495
+
496
+ render(<TestComponent
497
+ content="<div>test</div>"
498
+ onStateChange={(state) => { validationState = state; }}
499
+ />);
500
+
501
+ await act(async () => {
502
+ jest.advanceTimersByTime(500);
503
+ await Promise.resolve();
504
+ });
505
+
506
+ const criticalErrors = validationState.getErrorsBySeverity('critical');
507
+ expect(Array.isArray(criticalErrors)).toBe(true);
508
+ expect(criticalErrors.length).toBe(0);
509
+ });
510
+ });
511
+
512
+ describe('getErrorsBySource', () => {
513
+ it('returns function that filters by source', async () => {
514
+ let validationState;
515
+
516
+ render(<TestComponent
517
+ content="<div>test</div>"
518
+ onStateChange={(state) => { validationState = state; }}
519
+ />);
520
+
521
+ await act(async () => {
522
+ jest.advanceTimersByTime(500);
523
+ await Promise.resolve();
524
+ });
525
+
526
+ const htmlErrors = validationState.getErrorsBySource('htmlhint');
527
+ const cssErrors = validationState.getErrorsBySource('css-validator');
528
+
529
+ expect(Array.isArray(htmlErrors)).toBe(true);
530
+ expect(Array.isArray(cssErrors)).toBe(true);
531
+ });
532
+
533
+ it('returns empty array for non-existent source', async () => {
534
+ let validationState;
535
+
536
+ render(<TestComponent
537
+ content="<div>test</div>"
538
+ onStateChange={(state) => { validationState = state; }}
539
+ />);
540
+
541
+ await act(async () => {
542
+ jest.advanceTimersByTime(500);
543
+ await Promise.resolve();
544
+ });
545
+
546
+ const unknownErrors = validationState.getErrorsBySource('unknown-source');
547
+ expect(Array.isArray(unknownErrors)).toBe(true);
548
+ expect(unknownErrors.length).toBe(0);
549
+ });
550
+ });
551
+
552
+ describe('getAllIssues', () => {
553
+ it('returns combined and sorted issues', async () => {
554
+ let validationState;
555
+
556
+ render(<TestComponent
557
+ content="<div>test</div>"
558
+ onStateChange={(state) => { validationState = state; }}
559
+ />);
560
+
561
+ await act(async () => {
562
+ jest.advanceTimersByTime(500);
563
+ await Promise.resolve();
564
+ });
565
+
566
+ const allIssues = validationState.getAllIssues();
567
+
568
+ expect(Array.isArray(allIssues)).toBe(true);
569
+ });
570
+
571
+ it('returns empty array when no issues exist', async () => {
572
+ let validationState;
573
+
574
+ render(<TestComponent
575
+ content=""
576
+ onStateChange={(state) => { validationState = state; }}
577
+ />);
578
+
579
+ await act(async () => {
580
+ jest.advanceTimersByTime(500);
581
+ await Promise.resolve();
582
+ });
583
+
584
+ const allIssues = validationState.getAllIssues();
585
+ expect(Array.isArray(allIssues)).toBe(true);
586
+ });
587
+ });
588
+
589
+ describe('isClean', () => {
590
+ it('returns boolean indicating clean state', async () => {
591
+ let validationState;
592
+
593
+ render(<TestComponent
594
+ content="<div>test</div>"
595
+ onStateChange={(state) => { validationState = state; }}
596
+ />);
597
+
598
+ await act(async () => {
599
+ jest.advanceTimersByTime(500);
600
+ await Promise.resolve();
601
+ });
602
+
603
+ const isClean = validationState.isClean();
604
+ expect(typeof isClean).toBe('boolean');
605
+ });
606
+
607
+ it('works consistently with computed property', async () => {
608
+ let validationState;
609
+
610
+ render(<TestComponent
611
+ content="<div>test</div>"
612
+ onStateChange={(state) => { validationState = state; }}
613
+ />);
614
+
615
+ await act(async () => {
616
+ jest.advanceTimersByTime(500);
617
+ await Promise.resolve();
618
+ });
619
+
620
+ const methodResult = validationState.isClean();
621
+ const computedResult = screen.getByTestId('is-clean').textContent === 'true';
622
+
623
+ expect(methodResult).toBe(computedResult);
624
+ });
625
+ });
626
+ });
627
+
628
+ describe('Edge Cases and Error Handling', () => {
629
+ it('handles empty content gracefully', async () => {
630
+ render(<TestComponent content="" />);
631
+
632
+ await act(async () => {
633
+ jest.advanceTimersByTime(500);
634
+ await Promise.resolve();
635
+ });
636
+
637
+ expect(screen.getByTestId('is-valid')).toHaveTextContent('true');
638
+ expect(screen.getByTestId('is-secure')).toHaveTextContent('true');
639
+ expect(screen.getByTestId('total-errors')).toHaveTextContent('0');
640
+ });
641
+
642
+ it('handles null content gracefully', async () => {
643
+ render(<TestComponent content={null} />);
644
+
645
+ await act(async () => {
646
+ jest.advanceTimersByTime(500);
647
+ await Promise.resolve();
648
+ });
649
+
650
+ expect(screen.getByTestId('is-valid')).toHaveTextContent('true');
651
+ expect(screen.getByTestId('is-secure')).toHaveTextContent('true');
652
+ });
653
+
654
+ it('handles undefined content gracefully', async () => {
655
+ render(<TestComponent content={undefined} />);
656
+
657
+ await act(async () => {
658
+ jest.advanceTimersByTime(500);
659
+ await Promise.resolve();
660
+ });
661
+
662
+ expect(screen.getByTestId('is-valid')).toHaveTextContent('true');
663
+ expect(screen.getByTestId('is-secure')).toHaveTextContent('true');
664
+ });
665
+
666
+ it('handles very large content', async () => {
667
+ const largeContent = '<div>' + 'a'.repeat(100000) + '</div>';
668
+
669
+ render(<TestComponent content={largeContent} />);
670
+
671
+ await act(async () => {
672
+ jest.advanceTimersByTime(500);
673
+ await Promise.resolve();
674
+ });
675
+
676
+ expect(screen.getByTestId('last-validated')).toHaveTextContent('validated');
677
+ });
678
+
679
+ it('handles rapid content changes', async () => {
680
+ const { rerender } = render(<TestComponent content="<div>content1</div>" />);
681
+
682
+ // Rapid changes
683
+ await act(async () => {
684
+ rerender(<TestComponent content="<div>content2</div>" />);
685
+ rerender(<TestComponent content="<div>content3</div>" />);
686
+ rerender(<TestComponent content="<div>final-content</div>" />);
687
+
688
+ jest.advanceTimersByTime(500);
689
+ await Promise.resolve();
690
+ });
691
+
692
+ expect(screen.getByTestId('last-validated')).toHaveTextContent('validated');
693
+ });
694
+
695
+ it('handles validation errors gracefully', async () => {
696
+ // This test verifies that the hook handles validation errors without crashing
697
+ // We can't easily mock the validation to throw errors, so we test the error handling path
698
+ // by ensuring the hook completes validation even with potentially problematic content
699
+
700
+ render(<TestComponent content="<div>potentially problematic content</div>" />);
701
+
702
+ await act(async () => {
703
+ jest.advanceTimersByTime(500);
704
+ await Promise.resolve();
705
+ });
706
+
707
+ // Should handle any validation issues gracefully and complete the process
708
+ expect(screen.getByTestId('is-validating')).toHaveTextContent('false');
709
+ expect(screen.getByTestId('last-validated')).toHaveTextContent('validated');
710
+ });
711
+ });
712
+
713
+ describe('Performance and Memory Management', () => {
714
+ it('cleans up timers on unmount', async () => {
715
+ const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout');
716
+
717
+ const { unmount } = render(<TestComponent content="<div>test</div>" />);
718
+
719
+ unmount();
720
+
721
+ expect(clearTimeoutSpy).toHaveBeenCalled();
722
+
723
+ clearTimeoutSpy.mockRestore();
724
+ });
725
+
726
+ it('handles multiple simultaneous validations', async () => {
727
+ render(
728
+ <div>
729
+ <TestComponent content="<div>content1</div>" />
730
+ <TestComponent content="<div>content2</div>" />
731
+ <TestComponent content="<div>content3</div>" />
732
+ </div>
733
+ );
734
+
735
+ await act(async () => {
736
+ jest.advanceTimersByTime(500);
737
+ await Promise.resolve();
738
+ });
739
+
740
+ // All components should validate independently
741
+ const validElements = screen.getAllByTestId('is-valid');
742
+ expect(validElements).toHaveLength(3);
743
+ });
744
+
745
+ it('maintains performance with frequent updates', async () => {
746
+ const { rerender } = render(<TestComponent content="<div>initial</div>" />);
747
+
748
+ const startTime = performance.now();
749
+
750
+ // Simulate frequent updates
751
+ for (let i = 0; i < 20; i++) {
752
+ rerender(<TestComponent content={`<div>content-${i}</div>`} />);
753
+ }
754
+
755
+ await act(async () => {
756
+ jest.advanceTimersByTime(500);
757
+ await Promise.resolve();
758
+ });
759
+
760
+ const endTime = performance.now();
761
+ const duration = endTime - startTime;
762
+
763
+ // Should complete reasonably quickly
764
+ expect(duration).toBeLessThan(1000);
765
+ expect(screen.getByTestId('last-validated')).toHaveTextContent('validated');
766
+ });
767
+ });
768
+ });