@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.
- package/assets/Android.png +0 -0
- package/assets/iOS.png +0 -0
- package/package.json +16 -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 +403 -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/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/Rcs/index.js +2 -0
- 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
- 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
|
+
});
|