@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,741 @@
1
+ /**
2
+ * contentSanitizer Tests
3
+ *
4
+ * Tests for HTML content sanitization and security validation
5
+ */
6
+
7
+ import {
8
+ sanitizeHTML,
9
+ prepareForProduction,
10
+ isContentSafe,
11
+ findUnsafeContent
12
+ } from '../contentSanitizer';
13
+
14
+ // Mock DOMPurify
15
+ jest.mock('dompurify', () => {
16
+ // Global hooks array for legacy API compatibility
17
+ let globalHooks = [];
18
+
19
+ const createMockSanitize = (hooksArray) => (html, config) => {
20
+ if (!html || typeof html !== 'string') {
21
+ return '';
22
+ }
23
+
24
+ // Simulate DOMPurify behavior
25
+ let sanitized = html;
26
+
27
+ // Check for and remove script tags
28
+ if (/<script/gi.test(html)) {
29
+ sanitized = sanitized.replace(/<script[^>]*>.*?<\/script>/gi, '');
30
+ hooksArray.forEach(hook => {
31
+ if (hook.event === 'uponSanitizeElement') {
32
+ hook.callback({ tagName: 'script' }, { allowedTags: { script: false }, tagName: 'script' });
33
+ }
34
+ });
35
+ }
36
+
37
+ // Check for and remove event handlers
38
+ if (/\s*on\w+\s*=/gi.test(html)) {
39
+ sanitized = sanitized.replace(/\s*on\w+\s*=\s*["'][^"']*["']/gi, '');
40
+ hooksArray.forEach(hook => {
41
+ if (hook.event === 'uponSanitizeAttribute') {
42
+ hook.callback({ tagName: 'div' }, { attrName: 'onclick', attrValue: 'alert(1)', forceKeepAttr: false });
43
+ }
44
+ });
45
+ }
46
+
47
+ // Check for and remove javascript: protocols
48
+ if (/javascript:/gi.test(html)) {
49
+ sanitized = sanitized.replace(/javascript:/gi, '');
50
+ hooksArray.forEach(hook => {
51
+ if (hook.event === 'uponSanitizeAttribute') {
52
+ hook.callback({ tagName: 'a' }, { attrName: 'href', attrValue: 'javascript:void(0)', forceKeepAttr: false });
53
+ }
54
+ });
55
+ }
56
+
57
+ // Check for and remove iframe tags
58
+ if (/<iframe/gi.test(html)) {
59
+ sanitized = sanitized.replace(/<iframe[^>]*>.*?<\/iframe>/gi, '');
60
+ hooksArray.forEach(hook => {
61
+ if (hook.event === 'uponSanitizeElement') {
62
+ hook.callback({ tagName: 'iframe' }, { allowedTags: { iframe: false }, tagName: 'iframe' });
63
+ }
64
+ });
65
+ }
66
+
67
+ return sanitized;
68
+ };
69
+
70
+ // Create a factory function that returns DOMPurify instances with their own hooks
71
+ const createInstance = () => {
72
+ const instanceHooks = []; // Each instance gets its own hooks array
73
+
74
+ return {
75
+ sanitize: createMockSanitize(instanceHooks),
76
+ addHook: (event, callback) => {
77
+ instanceHooks.push({ event, callback });
78
+ },
79
+ removeHook: (event, callback) => {
80
+ const index = instanceHooks.findIndex(hook => hook.event === event && hook.callback === callback);
81
+ if (index !== -1) {
82
+ instanceHooks.splice(index, 1);
83
+ }
84
+ },
85
+ removeAllHooks: () => {
86
+ instanceHooks.length = 0; // Clear the instance hooks array
87
+ }
88
+ };
89
+ };
90
+
91
+ return {
92
+ default: createInstance, // DOMPurify() creates instances
93
+ sanitize: createMockSanitize(globalHooks), // Legacy global methods for backward compatibility
94
+ addHook: (event, callback) => {
95
+ globalHooks.push({ event, callback });
96
+ },
97
+ removeAllHooks: () => {
98
+ globalHooks.length = 0; // Clear the global hooks array
99
+ }
100
+ };
101
+ });
102
+
103
+ describe('contentSanitizer', () => {
104
+ beforeEach(() => {
105
+ // Reset global hooks before each test to prevent test pollution
106
+ const DOMPurify = require('dompurify');
107
+ if (DOMPurify.removeAllHooks) {
108
+ DOMPurify.removeAllHooks();
109
+ }
110
+
111
+ // Clear module cache to ensure fresh instances
112
+ jest.clearAllMocks();
113
+ });
114
+
115
+ describe('sanitizeHTML', () => {
116
+ describe('Basic Sanitization', () => {
117
+ it('sanitizes valid HTML content', () => {
118
+ const html = '<p>Hello World</p>';
119
+ const result = sanitizeHTML(html);
120
+
121
+ expect(result).toHaveProperty('sanitized');
122
+ expect(result).toHaveProperty('isClean');
123
+ expect(result).toHaveProperty('removedElements');
124
+ expect(result).toHaveProperty('warnings');
125
+ });
126
+
127
+ it('handles empty string', () => {
128
+ const result = sanitizeHTML('');
129
+
130
+ expect(result.sanitized).toBe('');
131
+ expect(result.isClean).toBe(true);
132
+ expect(result.warnings).toHaveLength(0);
133
+ });
134
+
135
+ it('handles null input', () => {
136
+ const result = sanitizeHTML(null);
137
+
138
+ expect(result.sanitized).toBe('');
139
+ expect(result.isClean).toBe(true);
140
+ });
141
+
142
+ it('handles undefined input', () => {
143
+ const result = sanitizeHTML(undefined);
144
+
145
+ expect(result.sanitized).toBe('');
146
+ expect(result.isClean).toBe(true);
147
+ });
148
+
149
+ it('handles non-string input', () => {
150
+ const result = sanitizeHTML(123);
151
+
152
+ expect(result.sanitized).toBe('');
153
+ expect(result.isClean).toBe(true);
154
+ });
155
+ });
156
+
157
+ describe('XSS Protection', () => {
158
+ it('removes script tags', () => {
159
+ const html = '<p>Hello</p><script>alert("xss")</script>';
160
+ const result = sanitizeHTML(html);
161
+
162
+ expect(result.sanitized).not.toContain('<script');
163
+ expect(result.isClean).toBe(false);
164
+ });
165
+
166
+ it('removes event handler attributes', () => {
167
+ const html = '<div onclick="alert(1)">Click me</div>';
168
+ const result = sanitizeHTML(html);
169
+
170
+ expect(result.sanitized).not.toContain('onclick');
171
+ expect(result.isClean).toBe(false);
172
+ });
173
+
174
+ it('removes javascript: protocol', () => {
175
+ const html = '<a href="javascript:void(0)">Link</a>';
176
+ const result = sanitizeHTML(html);
177
+
178
+ expect(result.sanitized).not.toContain('javascript:');
179
+ expect(result.isClean).toBe(false);
180
+ });
181
+
182
+ it('removes iframe tags', () => {
183
+ const html = '<iframe src="evil.com"></iframe>';
184
+ const result = sanitizeHTML(html);
185
+
186
+ expect(result.sanitized).not.toContain('iframe');
187
+ expect(result.isClean).toBe(false);
188
+ });
189
+
190
+ it('removes multiple dangerous elements', () => {
191
+ const html = `
192
+ <div onclick="alert(1)">
193
+ <script>alert("xss")</script>
194
+ <iframe src="evil.com"></iframe>
195
+ </div>
196
+ `;
197
+ const result = sanitizeHTML(html);
198
+
199
+ expect(result.sanitized).not.toContain('script');
200
+ expect(result.sanitized).not.toContain('iframe');
201
+ expect(result.sanitized).not.toContain('onclick');
202
+ });
203
+ });
204
+
205
+ describe('Variant-Specific Sanitization', () => {
206
+ it('uses email config for email variant', () => {
207
+ const html = '<p>Email content</p>';
208
+ const result = sanitizeHTML(html, 'email');
209
+
210
+ expect(result).toBeDefined();
211
+ expect(result.sanitized).toBeDefined();
212
+ expect(typeof result.sanitized).toBe('string');
213
+ // The mock should preserve safe HTML
214
+ if (result.sanitized.length > 0) {
215
+ expect(result.sanitized).toContain('Email content');
216
+ }
217
+ });
218
+
219
+ it('uses inapp config for inapp variant', () => {
220
+ const html = '<p>InApp content</p>';
221
+ const result = sanitizeHTML(html, 'inapp');
222
+
223
+ expect(result).toBeDefined();
224
+ expect(result.sanitized).toBeDefined();
225
+ expect(typeof result.sanitized).toBe('string');
226
+ // The mock should preserve safe HTML
227
+ if (result.sanitized.length > 0) {
228
+ expect(result.sanitized).toContain('InApp content');
229
+ }
230
+ });
231
+
232
+ it('defaults to email variant', () => {
233
+ const html = '<p>Default content</p>';
234
+ const result = sanitizeHTML(html);
235
+
236
+ expect(result).toBeDefined();
237
+ expect(result.sanitized).toBeDefined();
238
+ });
239
+ });
240
+
241
+ describe('Security Levels', () => {
242
+ it('applies standard level by default', () => {
243
+ const html = '<p>Standard content</p>';
244
+ const result = sanitizeHTML(html);
245
+
246
+ expect(result).toBeDefined();
247
+ });
248
+
249
+ it('applies strict level when specified', () => {
250
+ const html = '<p>Strict content</p>';
251
+ const result = sanitizeHTML(html, 'email', 'strict');
252
+
253
+ expect(result).toBeDefined();
254
+ });
255
+
256
+ it('strict level is more restrictive', () => {
257
+ const html = '<style>.test{}</style><p>Content</p>';
258
+ const resultStandard = sanitizeHTML(html, 'email', 'standard');
259
+ const resultStrict = sanitizeHTML(html, 'email', 'strict');
260
+
261
+ expect(resultStrict).toBeDefined();
262
+ expect(resultStandard).toBeDefined();
263
+ });
264
+ });
265
+
266
+ describe('Email Variant Warnings', () => {
267
+ it('warns about video elements in email', () => {
268
+ const html = '<video src="video.mp4"></video>';
269
+ const result = sanitizeHTML(html, 'email');
270
+
271
+ console.log('DEBUG - Video test result:', JSON.stringify(result, null, 2));
272
+ expect(result.warnings).toBeDefined();
273
+ expect(Array.isArray(result.warnings)).toBe(true);
274
+ // Video warnings are added by addVariantWarnings function
275
+ const hasVideoWarning = result.warnings.some(w =>
276
+ w.message && w.message.includes('Video/audio')
277
+ );
278
+ expect(hasVideoWarning).toBe(true);
279
+ });
280
+
281
+ it('warns about audio elements in email', () => {
282
+ const html = '<audio src="audio.mp3"></audio>';
283
+ const result = sanitizeHTML(html, 'email');
284
+
285
+ expect(result.warnings).toBeDefined();
286
+ const hasAudioWarning = result.warnings.some(w =>
287
+ w.message && w.message.includes('Video/audio')
288
+ );
289
+ expect(hasAudioWarning).toBe(true);
290
+ });
291
+
292
+ it('warns about fixed positioning in email', () => {
293
+ const html = '<div style="position: fixed">Fixed</div>';
294
+ const result = sanitizeHTML(html, 'email');
295
+
296
+ expect(result.warnings).toBeDefined();
297
+ const hasPositionWarning = result.warnings.some(w =>
298
+ w.message && w.message.includes('Fixed/sticky positioning')
299
+ );
300
+ expect(hasPositionWarning).toBe(true);
301
+ });
302
+
303
+ it('warns about sticky positioning in email', () => {
304
+ const html = '<div style="position: sticky">Sticky</div>';
305
+ const result = sanitizeHTML(html, 'email');
306
+
307
+ expect(result.warnings).toBeDefined();
308
+ const hasPositionWarning = result.warnings.some(w =>
309
+ w.message && w.message.includes('Fixed/sticky positioning')
310
+ );
311
+ expect(hasPositionWarning).toBe(true);
312
+ });
313
+ });
314
+
315
+ describe('InApp Variant Warnings', () => {
316
+ it('provides info about tables in mobile', () => {
317
+ const html = '<table><tr><td>Data</td></tr></table>';
318
+ const result = sanitizeHTML(html, 'inapp');
319
+
320
+ expect(result.warnings).toBeDefined();
321
+ const hasTableInfo = result.warnings.some(w =>
322
+ w.message && w.message.includes('Tables may not be mobile-friendly')
323
+ );
324
+ expect(hasTableInfo).toBe(true);
325
+ });
326
+
327
+ it('suggests relative font sizes for mobile', () => {
328
+ const html = '<div style="font-size: 14px">Text</div>';
329
+ const result = sanitizeHTML(html, 'inapp');
330
+
331
+ expect(result.warnings).toBeDefined();
332
+ const hasFontInfo = result.warnings.some(w =>
333
+ w.message && w.message.includes('relative font sizes')
334
+ );
335
+ expect(hasFontInfo).toBe(true);
336
+ });
337
+ });
338
+
339
+ describe('Removed Elements Tracking', () => {
340
+ it('tracks removed forbidden tags', () => {
341
+ const html = '<script>alert(1)</script>';
342
+ const result = sanitizeHTML(html);
343
+
344
+ // When dangerous content is removed, isClean should be false
345
+ expect(result.isClean).toBe(false);
346
+ // removedElements array should exist
347
+ expect(Array.isArray(result.removedElements)).toBe(true);
348
+ });
349
+
350
+ it('tracks removed event handlers', () => {
351
+ const html = '<div onclick="alert(1)">Test</div>';
352
+ const result = sanitizeHTML(html);
353
+
354
+ // Content should be marked as not clean
355
+ expect(result.isClean).toBe(false);
356
+ expect(Array.isArray(result.removedElements)).toBe(true);
357
+ });
358
+
359
+ it('tracks removed javascript protocols', () => {
360
+ const html = '<a href="javascript:void(0)">Link</a>';
361
+ const result = sanitizeHTML(html);
362
+
363
+ // Content should be marked as not clean
364
+ expect(result.isClean).toBe(false);
365
+ expect(Array.isArray(result.removedElements)).toBe(true);
366
+ });
367
+ });
368
+
369
+ describe('Error Handling', () => {
370
+ it('handles DOMPurify errors gracefully', () => {
371
+ // Since we can't easily mock a one-time error with our plain function mock,
372
+ // we'll test the error path by ensuring the function handles edge cases
373
+ const result = sanitizeHTML(null);
374
+
375
+ // Should handle null gracefully
376
+ expect(result.sanitized).toBe('');
377
+ expect(result.isClean).toBe(true);
378
+ expect(Array.isArray(result.warnings)).toBe(true);
379
+ });
380
+ });
381
+
382
+ describe('Content Modification Detection', () => {
383
+ it('detects when content is modified', () => {
384
+ const html = '<script>alert(1)</script><p>Safe</p>';
385
+ const result = sanitizeHTML(html);
386
+
387
+ expect(result.isClean).toBe(false);
388
+ });
389
+
390
+ it('marks clean content as clean', () => {
391
+ const html = '<p>Clean content</p>';
392
+ const result = sanitizeHTML(html);
393
+
394
+ // Note: isClean will be true only if sanitized === html
395
+ expect(result).toBeDefined();
396
+ });
397
+ });
398
+ });
399
+
400
+ describe('prepareForProduction', () => {
401
+ it('sanitizes content with strict rules', () => {
402
+ const html = '<p>Production content</p>';
403
+ const result = prepareForProduction(html);
404
+
405
+ expect(result).toHaveProperty('content');
406
+ expect(result).toHaveProperty('isProductionReady');
407
+ expect(result).toHaveProperty('securityIssues');
408
+ expect(result).toHaveProperty('warnings');
409
+ expect(result).toHaveProperty('recommendations');
410
+ });
411
+
412
+ it('marks unsafe content as not production ready', () => {
413
+ const html = '<script>alert(1)</script><p>Content</p>';
414
+ const result = prepareForProduction(html);
415
+
416
+ expect(result.isProductionReady).toBe(false);
417
+ expect(result.recommendations.length).toBeGreaterThan(0);
418
+ });
419
+
420
+ it('provides recommendations for sanitized content', () => {
421
+ const html = '<script>alert(1)</script><p>Content</p>';
422
+ const result = prepareForProduction(html);
423
+
424
+ const sanitizeRec = result.recommendations.find(r =>
425
+ r.includes('sanitized')
426
+ );
427
+ expect(sanitizeRec).toBeDefined();
428
+ });
429
+
430
+ it('recommends inlining CSS for email', () => {
431
+ const html = '<style>.test{}</style><p>Email</p>';
432
+ const result = prepareForProduction(html, 'email');
433
+
434
+ const cssRec = result.recommendations.find(r =>
435
+ r.includes('inlining CSS')
436
+ );
437
+ expect(cssRec).toBeDefined();
438
+ });
439
+
440
+ it('warns about large content for inapp', () => {
441
+ const largeHtml = '<p>' + 'a'.repeat(50001) + '</p>';
442
+ const result = prepareForProduction(largeHtml, 'inapp');
443
+
444
+ const sizeRec = result.recommendations.find(r =>
445
+ r.includes('large')
446
+ );
447
+ expect(sizeRec).toBeDefined();
448
+ });
449
+
450
+ it('handles email variant', () => {
451
+ const html = '<p>Email content</p>';
452
+ const result = prepareForProduction(html, 'email');
453
+
454
+ expect(result.content).toBeDefined();
455
+ });
456
+
457
+ it('handles inapp variant', () => {
458
+ const html = '<p>InApp content</p>';
459
+ const result = prepareForProduction(html, 'inapp');
460
+
461
+ expect(result.content).toBeDefined();
462
+ });
463
+
464
+ it('defaults to email variant', () => {
465
+ const html = '<p>Default content</p>';
466
+ const result = prepareForProduction(html);
467
+
468
+ expect(result.content).toBeDefined();
469
+ });
470
+ });
471
+
472
+ describe('isContentSafe', () => {
473
+ it('returns true for safe content', () => {
474
+ const html = '<p>Safe content</p>';
475
+ const result = isContentSafe(html);
476
+
477
+ expect(result).toBe(true);
478
+ });
479
+
480
+ it('returns true for empty content', () => {
481
+ const result = isContentSafe('');
482
+
483
+ expect(result).toBe(true);
484
+ });
485
+
486
+ it('returns true for null content', () => {
487
+ const result = isContentSafe(null);
488
+
489
+ expect(result).toBe(true);
490
+ });
491
+
492
+ it('detects javascript: protocol', () => {
493
+ const html = '<a href="javascript:void(0)">Link</a>';
494
+ const result = isContentSafe(html);
495
+
496
+ expect(result).toBe(false);
497
+ });
498
+
499
+ it('detects data: protocol', () => {
500
+ const html = '<img src="data:text/html,<script>alert(1)</script>">';
501
+ const result = isContentSafe(html);
502
+
503
+ expect(result).toBe(false);
504
+ });
505
+
506
+ it('detects vbscript: protocol', () => {
507
+ const html = '<a href="vbscript:msgbox">Link</a>';
508
+ const result = isContentSafe(html);
509
+
510
+ expect(result).toBe(false);
511
+ });
512
+
513
+ it('detects script tags', () => {
514
+ const html = '<script>alert(1)</script>';
515
+ const result = isContentSafe(html);
516
+
517
+ expect(result).toBe(false);
518
+ });
519
+
520
+ it('detects onclick handlers', () => {
521
+ const html = '<div onclick="alert(1)">Click</div>';
522
+ const result = isContentSafe(html);
523
+
524
+ expect(result).toBe(false);
525
+ });
526
+
527
+ it('detects onload handlers', () => {
528
+ const html = '<body onload="alert(1)">Content</body>';
529
+ const result = isContentSafe(html);
530
+
531
+ expect(result).toBe(false);
532
+ });
533
+
534
+ it('detects onerror handlers', () => {
535
+ const html = '<img src=x onerror="alert(1)">';
536
+ const result = isContentSafe(html);
537
+
538
+ expect(result).toBe(false);
539
+ });
540
+
541
+ it('detects onmouseover handlers', () => {
542
+ const html = '<div onmouseover="alert(1)">Hover</div>';
543
+ const result = isContentSafe(html);
544
+
545
+ expect(result).toBe(false);
546
+ });
547
+
548
+ it('detects iframe tags', () => {
549
+ const html = '<iframe src="evil.com"></iframe>';
550
+ const result = isContentSafe(html);
551
+
552
+ expect(result).toBe(false);
553
+ });
554
+
555
+ it('detects object tags', () => {
556
+ const html = '<object data="evil.swf"></object>';
557
+ const result = isContentSafe(html);
558
+
559
+ expect(result).toBe(false);
560
+ });
561
+
562
+ it('detects embed tags', () => {
563
+ const html = '<embed src="evil.swf">';
564
+ const result = isContentSafe(html);
565
+
566
+ expect(result).toBe(false);
567
+ });
568
+
569
+ it('is case insensitive', () => {
570
+ const html = '<SCRIPT>alert(1)</SCRIPT>';
571
+ const result = isContentSafe(html);
572
+
573
+ expect(result).toBe(false);
574
+ });
575
+ });
576
+
577
+ describe('findUnsafeContent', () => {
578
+ it('returns empty array for safe content', () => {
579
+ const html = '<p>Safe content</p>';
580
+ const result = findUnsafeContent(html);
581
+
582
+ expect(Array.isArray(result)).toBe(true);
583
+ expect(result).toHaveLength(0);
584
+ });
585
+
586
+ it('returns empty array for empty content', () => {
587
+ const result = findUnsafeContent('');
588
+
589
+ expect(result).toHaveLength(0);
590
+ });
591
+
592
+ it('returns empty array for null content', () => {
593
+ const result = findUnsafeContent(null);
594
+
595
+ expect(result).toHaveLength(0);
596
+ });
597
+
598
+ it('finds javascript protocol', () => {
599
+ const html = '<a href="javascript:void(0)">Link</a>';
600
+ const result = findUnsafeContent(html);
601
+
602
+ expect(result.length).toBeGreaterThan(0);
603
+ expect(result[0].type).toBe('JavaScript Protocol');
604
+ });
605
+
606
+ it('finds data URLs', () => {
607
+ const html = '<img src="data:image/png,...">';
608
+ const result = findUnsafeContent(html);
609
+
610
+ expect(result.length).toBeGreaterThan(0);
611
+ expect(result[0].type).toBe('Data URL');
612
+ });
613
+
614
+ it('finds script tags', () => {
615
+ const html = '<script>alert(1)</script>';
616
+ const result = findUnsafeContent(html);
617
+
618
+ expect(result.length).toBeGreaterThan(0);
619
+ const scriptTag = result.find(r => r.type === 'Script Tag');
620
+ expect(scriptTag).toBeDefined();
621
+ });
622
+
623
+ it('finds event handlers', () => {
624
+ const html = '<div onclick="alert(1)">Click</div>';
625
+ const result = findUnsafeContent(html);
626
+
627
+ expect(result.length).toBeGreaterThan(0);
628
+ const eventHandler = result.find(r => r.type === 'Event Handler');
629
+ expect(eventHandler).toBeDefined();
630
+ });
631
+
632
+ it('finds iframe tags', () => {
633
+ const html = '<iframe src="evil.com"></iframe>';
634
+ const result = findUnsafeContent(html);
635
+
636
+ expect(result.length).toBeGreaterThan(0);
637
+ const iframe = result.find(r => r.type === 'Iframe');
638
+ expect(iframe).toBeDefined();
639
+ });
640
+
641
+ it('finds object and embed tags', () => {
642
+ const html = '<object data="evil.swf"></object>';
643
+ const result = findUnsafeContent(html);
644
+
645
+ expect(result.length).toBeGreaterThan(0);
646
+ const objectTag = result.find(r => r.type === 'Object/Embed');
647
+ expect(objectTag).toBeDefined();
648
+ });
649
+
650
+ it('finds multiple unsafe patterns', () => {
651
+ const html = `
652
+ <script>alert(1)</script>
653
+ <div onclick="alert(2)">Click</div>
654
+ <iframe src="evil.com"></iframe>
655
+ `;
656
+ const result = findUnsafeContent(html);
657
+
658
+ expect(result.length).toBeGreaterThan(2);
659
+ });
660
+
661
+ it('includes content in results', () => {
662
+ const html = '<script>alert(1)</script>';
663
+ const result = findUnsafeContent(html);
664
+
665
+ expect(result[0]).toHaveProperty('content');
666
+ expect(result[0].content).toContain('script');
667
+ });
668
+
669
+ it('includes position in results', () => {
670
+ const html = '<p>Safe</p><script>alert(1)</script>';
671
+ const result = findUnsafeContent(html);
672
+
673
+ expect(result[0]).toHaveProperty('position');
674
+ expect(typeof result[0].position).toBe('number');
675
+ });
676
+
677
+ it('finds multiple occurrences of same pattern', () => {
678
+ const html = '<script>alert(1)</script><script>alert(2)</script>';
679
+ const result = findUnsafeContent(html);
680
+
681
+ const scriptTags = result.filter(r => r.type === 'Script Tag');
682
+ expect(scriptTags.length).toBe(2);
683
+ });
684
+
685
+ it('is case insensitive', () => {
686
+ const html = '<SCRIPT>alert(1)</SCRIPT>';
687
+ const result = findUnsafeContent(html);
688
+
689
+ expect(result.length).toBeGreaterThan(0);
690
+ });
691
+ });
692
+
693
+ describe('Integration Tests', () => {
694
+ it('sanitizes and validates content in sequence', () => {
695
+ const html = '<script>alert(1)</script><p>Safe content</p>';
696
+
697
+ const isSafe = isContentSafe(html);
698
+ expect(isSafe).toBe(false);
699
+
700
+ const unsafe = findUnsafeContent(html);
701
+ expect(unsafe.length).toBeGreaterThan(0);
702
+
703
+ const sanitized = sanitizeHTML(html);
704
+ expect(sanitized.sanitized).not.toContain('script');
705
+
706
+ const preparedResult = prepareForProduction(html);
707
+ expect(preparedResult.isProductionReady).toBe(false);
708
+ });
709
+
710
+ it('handles complex email template', () => {
711
+ const html = `
712
+ <html>
713
+ <head>
714
+ <style>.header{color:blue}</style>
715
+ </head>
716
+ <body>
717
+ <table>
718
+ <tr><td>Email Content</td></tr>
719
+ </table>
720
+ </body>
721
+ </html>
722
+ `;
723
+
724
+ const result = sanitizeHTML(html, 'email');
725
+ expect(result.sanitized).toBeDefined();
726
+ });
727
+
728
+ it('handles complex inapp template', () => {
729
+ const html = `
730
+ <div style="font-size: 16px">
731
+ <video src="video.mp4"></video>
732
+ <table><tr><td>Data</td></tr></table>
733
+ </div>
734
+ `;
735
+
736
+ const result = sanitizeHTML(html, 'inapp');
737
+ expect(result.warnings.length).toBeGreaterThan(0);
738
+ });
739
+ });
740
+ });
741
+