@capillarytech/creatives-library 8.0.299-alpha.6 → 8.0.299-alpha.7
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/package.json +1 -1
- package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +31 -114
- package/v2Components/CommonTestAndPreview/index.js +1 -16
- package/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +320 -0
- package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +132 -0
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +1360 -1480
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +309 -339
- package/v2Containers/TemplatesV2/TemplatesV2.style.js +3 -9
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +5101 -5441
package/package.json
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
/* All ant overrides scoped under wrapper to avoid affecting other modals. */
|
|
9
9
|
.common-test-preview-modal-wrap {
|
|
10
|
-
z-index: 10000
|
|
11
|
-
display: flex
|
|
12
|
-
justify-content: center
|
|
13
|
-
align-items: center
|
|
10
|
+
z-index: 10000;
|
|
11
|
+
display: flex;
|
|
12
|
+
justify-content: center;
|
|
13
|
+
align-items: center;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/* Modals rendered inside slidebox via getContainer (lib mode) – mask and wrap above content */
|
|
@@ -19,14 +19,13 @@
|
|
|
19
19
|
z-index: 10001;
|
|
20
20
|
}
|
|
21
21
|
.common-test-preview-modal-wrap {
|
|
22
|
-
z-index: 10003
|
|
22
|
+
z-index: 10003;
|
|
23
23
|
}
|
|
24
24
|
.ant-modal-mask,
|
|
25
25
|
.ant-modal-wrap {
|
|
26
|
-
z-index: 10003
|
|
26
|
+
z-index: 10003;
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
|
|
30
29
|
/* Lookup spinner overlay above test-customers dropdown. */
|
|
31
30
|
.common-test-preview-lookup-spin {
|
|
32
31
|
position: relative;
|
|
@@ -39,14 +38,12 @@
|
|
|
39
38
|
|
|
40
39
|
/* When customer lookup is loading, dropdown renders inside .send-test-section; lower it so spinner is on top. */
|
|
41
40
|
.common-test-preview-customer-loading .test-customers-tree-select-dropdown {
|
|
42
|
-
z-index: 0
|
|
41
|
+
z-index: 0;
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
/* Customer creation modal content – avoid inline styles */
|
|
46
45
|
.common-test-preview-modal {
|
|
47
46
|
color: $CAP_G01 !important;
|
|
48
|
-
/* Modal: 14px base so em values are independent of root (width 32.57em = 456px) */
|
|
49
|
-
font-size: 14px;
|
|
50
47
|
width: 32.571em !important;
|
|
51
48
|
margin-left: auto;
|
|
52
49
|
margin-right: auto;
|
|
@@ -85,92 +82,40 @@
|
|
|
85
82
|
|
|
86
83
|
/* Input text color and font to match standalone (lib mode can lose host styles) */
|
|
87
84
|
.customer-creation-modal-input {
|
|
88
|
-
width: 100%;
|
|
89
|
-
color: $CAP_G01;
|
|
90
|
-
font-size: $FONT_SIZE_M;
|
|
91
|
-
font-family: 'Roboto', Arial, sans-serif;
|
|
92
|
-
font-weight: normal;
|
|
93
85
|
border: none !important;
|
|
94
|
-
border-bottom: none !important;
|
|
95
|
-
box-shadow: none !important;
|
|
96
|
-
|
|
97
86
|
/* Sizes in em for 14px base: same px as .5rem 2.5rem 1.5rem .875rem at 16px (8px, 40px, 24px, 14px) */
|
|
98
87
|
.ant-input {
|
|
99
|
-
color: $CAP_G01
|
|
100
|
-
font-size: $FONT_SIZE_M
|
|
101
|
-
font-
|
|
102
|
-
font-weight: normal !important;
|
|
103
|
-
background-color: $CAP_WHITE !important;
|
|
88
|
+
color: $CAP_G01;
|
|
89
|
+
font-size: $FONT_SIZE_M;
|
|
90
|
+
font-weight: normal;
|
|
104
91
|
padding: 0.571em; /* 8px */
|
|
105
92
|
height: 2.857em; /* 40px */
|
|
106
93
|
line-height: 1.714em; /* 24px */
|
|
107
94
|
}
|
|
108
95
|
.ant-input:hover,
|
|
109
96
|
.ant-input:focus {
|
|
110
|
-
|
|
97
|
+
border: $CAP_SPACE_01 solid $CAP_G01 !important;
|
|
111
98
|
}
|
|
112
99
|
.ant-input::placeholder {
|
|
113
|
-
color: $
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/* No darkening on hover/focus – wrapper and inner stay same background */
|
|
117
|
-
& > *:hover,
|
|
118
|
-
& > *:focus,
|
|
119
|
-
& > *.ant-input-affix-wrapper-focused .ant-input {
|
|
120
|
-
background-color: $CAP_WHITE !important;
|
|
100
|
+
color: $CAP_G06;
|
|
121
101
|
}
|
|
122
102
|
|
|
123
103
|
/* Error state: single clean red border, no double line or focus ring */
|
|
124
104
|
&.has-error {
|
|
125
|
-
color: $CAP_COLOR_03 !important;
|
|
126
|
-
border-bottom: none !important;
|
|
127
|
-
box-shadow: none !important;
|
|
128
|
-
|
|
129
|
-
& > *::after {
|
|
130
|
-
display: none !important;
|
|
131
|
-
content: none !important;
|
|
132
|
-
border: none !important;
|
|
133
|
-
height: 0 !important;
|
|
134
|
-
background: none !important;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
105
|
.ant-input-affix-wrapper {
|
|
138
|
-
border: 1px solid $CAP_COLOR_03
|
|
106
|
+
border: 1px solid $CAP_COLOR_03;
|
|
139
107
|
border-radius: $CAP_SPACE_04;
|
|
140
|
-
box-shadow: none !important;
|
|
141
|
-
outline: none !important;
|
|
142
|
-
background-color: $CAP_WHITE !important;
|
|
143
108
|
}
|
|
144
109
|
.ant-input-affix-wrapper:hover,
|
|
145
110
|
.ant-input-affix-wrapper:focus,
|
|
146
111
|
.ant-input-affix-wrapper-focused {
|
|
147
|
-
border: 1px solid $CAP_COLOR_03
|
|
148
|
-
box-shadow: none !important;
|
|
149
|
-
outline: none !important;
|
|
150
|
-
background-color: $CAP_WHITE !important;
|
|
112
|
+
border: 1px solid $CAP_COLOR_03;
|
|
151
113
|
}
|
|
152
114
|
.ant-input-affix-wrapper .ant-input {
|
|
153
115
|
border: none !important;
|
|
154
|
-
box-shadow: none !important;
|
|
155
|
-
background-color: $CAP_WHITE !important;
|
|
156
|
-
}
|
|
157
|
-
.ant-input-affix-wrapper .ant-input:hover,
|
|
158
|
-
.ant-input-affix-wrapper .ant-input:focus {
|
|
159
|
-
background-color: $CAP_WHITE !important;
|
|
160
116
|
}
|
|
161
|
-
/* When no affix wrapper (plain input) */
|
|
162
117
|
.ant-input {
|
|
163
|
-
border: 1px solid $CAP_COLOR_03
|
|
164
|
-
border-radius: $CAP_SPACE_04;
|
|
165
|
-
box-shadow: none !important;
|
|
166
|
-
outline: none !important;
|
|
167
|
-
background-color: $CAP_WHITE !important;
|
|
168
|
-
}
|
|
169
|
-
.ant-input:hover,
|
|
170
|
-
.ant-input:focus {
|
|
171
|
-
box-shadow: none !important;
|
|
172
|
-
outline: none !important;
|
|
173
|
-
background-color: $CAP_WHITE !important;
|
|
118
|
+
border: 1px solid $CAP_COLOR_03;
|
|
174
119
|
}
|
|
175
120
|
}
|
|
176
121
|
}
|
|
@@ -180,55 +125,24 @@
|
|
|
180
125
|
color: $CAP_COLOR_03;
|
|
181
126
|
font-size: $FONT_SIZE_S;
|
|
182
127
|
margin-top: $CAP_SPACE_04;
|
|
183
|
-
border: none
|
|
184
|
-
border-top: none
|
|
185
|
-
box-shadow: none
|
|
128
|
+
border: none ;
|
|
129
|
+
border-top: none ;
|
|
130
|
+
box-shadow: none ;
|
|
186
131
|
|
|
187
132
|
&::before,
|
|
188
133
|
&::after {
|
|
189
|
-
display: none
|
|
190
|
-
content: none
|
|
191
|
-
border: none
|
|
134
|
+
display: none ;
|
|
135
|
+
content: none ;
|
|
136
|
+
border: none ;
|
|
192
137
|
}
|
|
193
138
|
}
|
|
194
139
|
|
|
195
|
-
/* Remove any extra underline/border between input and error message – scoped via wrapper only */
|
|
196
|
-
.customer-creation-modal-input.has-error + .customer-creation-modal-validation-error::before {
|
|
197
|
-
display: none !important;
|
|
198
|
-
border: none !important;
|
|
199
|
-
content: none !important;
|
|
200
|
-
}
|
|
201
|
-
.customer-creation-modal-row .customer-creation-modal-validation-error {
|
|
202
|
-
border-top: none !important;
|
|
203
|
-
box-shadow: none !important;
|
|
204
|
-
}
|
|
205
|
-
.customer-creation-modal-row:has(.customer-creation-modal-input.has-error) {
|
|
206
|
-
border-bottom: none !important;
|
|
207
|
-
box-shadow: none !important;
|
|
208
|
-
.customer-creation-modal-validation-error {
|
|
209
|
-
border-top: none !important;
|
|
210
|
-
box-shadow: none !important;
|
|
211
|
-
}
|
|
212
|
-
.customer-creation-modal-input {
|
|
213
|
-
border-bottom: none !important;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
/* Kill any pseudo-element line inside input wrapper */
|
|
217
|
-
.customer-creation-modal-row:has(.customer-creation-modal-input.has-error) .customer-creation-modal-input *::after {
|
|
218
|
-
display: none !important;
|
|
219
|
-
content: none !important;
|
|
220
|
-
border: none !important;
|
|
221
|
-
height: 0 !important;
|
|
222
|
-
min-height: 0 !important;
|
|
223
|
-
background: none !important;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
140
|
/* Lookup loading: make Email and Mobile fields look disabled (wrapper-only) */
|
|
227
141
|
.customer-creation-modal--lookup-loading .customer-creation-modal-row--email .customer-creation-modal-input,
|
|
228
142
|
.customer-creation-modal--lookup-loading .customer-creation-modal-row--last .customer-creation-modal-input {
|
|
229
143
|
opacity: 0.65;
|
|
230
144
|
cursor: not-allowed;
|
|
231
|
-
background-color: $CAP_G09
|
|
145
|
+
background-color: $CAP_G09;
|
|
232
146
|
}
|
|
233
147
|
|
|
234
148
|
/* Existing customer modal content */
|
|
@@ -237,13 +151,17 @@
|
|
|
237
151
|
margin-bottom: $CAP_SPACE_16;
|
|
238
152
|
}
|
|
239
153
|
|
|
154
|
+
.ant-card.cap-card-v2{
|
|
155
|
+
border:none
|
|
156
|
+
}
|
|
157
|
+
|
|
240
158
|
.ant-card-body {
|
|
241
159
|
padding: 1rem;
|
|
160
|
+
border-radius: $CAP_SPACE_08;
|
|
161
|
+
border: $CAP_SPACE_01 solid $CAP_G06;
|
|
242
162
|
}
|
|
243
163
|
|
|
244
164
|
&-card {
|
|
245
|
-
border-radius: $CAP_SPACE_08;
|
|
246
|
-
border: $CAP_SPACE_01 solid $CAP_G06;
|
|
247
165
|
padding: 0;
|
|
248
166
|
}
|
|
249
167
|
|
|
@@ -687,11 +605,10 @@
|
|
|
687
605
|
}
|
|
688
606
|
|
|
689
607
|
// Test customers TreeSelect dropdown: limit height and make scrollable (dropdown renders in portal)
|
|
690
|
-
// min-width so dropdown looks the same in campaigns and creatives
|
|
691
608
|
.test-customers-tree-select-dropdown {
|
|
692
|
-
min-width: 18rem
|
|
693
|
-
max-height: 20rem
|
|
694
|
-
overflow-y: auto
|
|
609
|
+
min-width: 18rem ;
|
|
610
|
+
max-height: 20rem ; /* 320px */
|
|
611
|
+
overflow-y: auto ;
|
|
695
612
|
.ant-select-tree-list-holder-inner {
|
|
696
613
|
overflow: visible;
|
|
697
614
|
}
|
|
@@ -171,9 +171,6 @@ const CommonTestAndPreview = (props) => {
|
|
|
171
171
|
const [selectedTestEntities, setSelectedTestEntities] = useState([]);
|
|
172
172
|
const [beeContent, setBeeContent] = useState(''); // Track BEE editor content separately (EMAIL only)
|
|
173
173
|
const previousBeeContentRef = useRef(''); // Track previous BEE content (EMAIL only)
|
|
174
|
-
// Container for notifications so they render inside the slidebox (visible in campaigns/library mode)
|
|
175
|
-
const notificationContainerRef = useRef(null);
|
|
176
|
-
const getNotificationContainer = () => notificationContainerRef.current || document.body;
|
|
177
174
|
// Delivery settings for Test and Preview (SMS, Email, WhatsApp) — user selection only
|
|
178
175
|
const [testPreviewDeliverySettings, setTestPreviewDeliverySettings] = useState({
|
|
179
176
|
[CHANNELS.SMS]: {
|
|
@@ -447,7 +444,6 @@ const CommonTestAndPreview = (props) => {
|
|
|
447
444
|
if (response && response.success) {
|
|
448
445
|
CapNotification.success({
|
|
449
446
|
message: formatMessage(messages.newTestCustomerAddedSuccess),
|
|
450
|
-
getContainer: getNotificationContainer,
|
|
451
447
|
});
|
|
452
448
|
// API may return customerId in response.response (e.g. { response: { customerId: 438845651 } })
|
|
453
449
|
const res = response?.response || response;
|
|
@@ -470,7 +466,6 @@ const CommonTestAndPreview = (props) => {
|
|
|
470
466
|
CapNotification.error({
|
|
471
467
|
message: formatMessage(messages.errorTitle),
|
|
472
468
|
description: response?.message || formatMessage(messages.failedToAddTestCustomer),
|
|
473
|
-
getContainer: getNotificationContainer,
|
|
474
469
|
});
|
|
475
470
|
}
|
|
476
471
|
} catch (error) {
|
|
@@ -479,7 +474,6 @@ const CommonTestAndPreview = (props) => {
|
|
|
479
474
|
CapNotification.error({
|
|
480
475
|
message: formatMessage(messages.errorTitle),
|
|
481
476
|
description: error?.message || formatMessage(messages.errorAddingTestCustomer),
|
|
482
|
-
getContainer: getNotificationContainer,
|
|
483
477
|
});
|
|
484
478
|
}
|
|
485
479
|
} finally {
|
|
@@ -2528,7 +2522,6 @@ const CommonTestAndPreview = (props) => {
|
|
|
2528
2522
|
} catch (error) {
|
|
2529
2523
|
CapNotification.error({
|
|
2530
2524
|
message: formatMessage(messages.invalidJSON),
|
|
2531
|
-
getContainer: getNotificationContainer,
|
|
2532
2525
|
});
|
|
2533
2526
|
}
|
|
2534
2527
|
};
|
|
@@ -2575,7 +2568,6 @@ const CommonTestAndPreview = (props) => {
|
|
|
2575
2568
|
} catch (error) {
|
|
2576
2569
|
CapNotification.error({
|
|
2577
2570
|
message: formatMessage(messages.previewUpdateError),
|
|
2578
|
-
getContainer: getNotificationContainer,
|
|
2579
2571
|
});
|
|
2580
2572
|
}
|
|
2581
2573
|
};
|
|
@@ -2669,7 +2661,6 @@ const CommonTestAndPreview = (props) => {
|
|
|
2669
2661
|
setSearchValue('');
|
|
2670
2662
|
CapNotification.success({
|
|
2671
2663
|
message: formatMessage(messages.customerAlreadyInTestList),
|
|
2672
|
-
getContainer: getNotificationContainer,
|
|
2673
2664
|
});
|
|
2674
2665
|
return;
|
|
2675
2666
|
}
|
|
@@ -2685,7 +2676,7 @@ const CommonTestAndPreview = (props) => {
|
|
|
2685
2676
|
|
|
2686
2677
|
if (!success) {
|
|
2687
2678
|
const errorMessage = response?.message || response?.status?.message || formatMessage(messages.memberLookupError);
|
|
2688
|
-
CapNotification.error({ title: formatMessage(messages.errorTitle), message: errorMessage
|
|
2679
|
+
CapNotification.error({ title: formatMessage(messages.errorTitle), message: errorMessage });
|
|
2689
2680
|
return;
|
|
2690
2681
|
}
|
|
2691
2682
|
|
|
@@ -2702,7 +2693,6 @@ const CommonTestAndPreview = (props) => {
|
|
|
2702
2693
|
setSearchValue('');
|
|
2703
2694
|
CapNotification.success({
|
|
2704
2695
|
message: formatMessage(messages.customerAlreadyInTestList),
|
|
2705
|
-
getContainer: getNotificationContainer,
|
|
2706
2696
|
});
|
|
2707
2697
|
return;
|
|
2708
2698
|
}
|
|
@@ -2720,7 +2710,6 @@ const CommonTestAndPreview = (props) => {
|
|
|
2720
2710
|
} catch {
|
|
2721
2711
|
CapNotification.error({
|
|
2722
2712
|
message: formatMessage(messages.memberLookupError),
|
|
2723
|
-
getContainer: getNotificationContainer,
|
|
2724
2713
|
});
|
|
2725
2714
|
} finally {
|
|
2726
2715
|
setIsCustomerDataLoading(false);
|
|
@@ -2773,12 +2762,10 @@ const CommonTestAndPreview = (props) => {
|
|
|
2773
2762
|
if (result) {
|
|
2774
2763
|
CapNotification.success({
|
|
2775
2764
|
message: formatMessage(messages.testMessageSent),
|
|
2776
|
-
getContainer: getNotificationContainer,
|
|
2777
2765
|
});
|
|
2778
2766
|
} else {
|
|
2779
2767
|
CapNotification.error({
|
|
2780
2768
|
message: formatMessage(messages.testMessageFailed),
|
|
2781
|
-
getContainer: getNotificationContainer,
|
|
2782
2769
|
});
|
|
2783
2770
|
}
|
|
2784
2771
|
});
|
|
@@ -2900,7 +2887,6 @@ const CommonTestAndPreview = (props) => {
|
|
|
2900
2887
|
show={show}
|
|
2901
2888
|
size="size-xl"
|
|
2902
2889
|
content={(
|
|
2903
|
-
<div ref={notificationContainerRef} className="common-test-and-preview-notification-container" style={{ position: 'relative', height: '100%' }}>
|
|
2904
2890
|
<CapSpin
|
|
2905
2891
|
spinning={isCustomerDataLoading}
|
|
2906
2892
|
className={`common-test-preview-lookup-spin ${isCustomerDataLoading ? 'common-test-preview-customer-loading' : ''}`}
|
|
@@ -2947,7 +2933,6 @@ const CommonTestAndPreview = (props) => {
|
|
|
2947
2933
|
)}
|
|
2948
2934
|
</CapRow>
|
|
2949
2935
|
</CapSpin>
|
|
2950
|
-
</div>
|
|
2951
2936
|
)}
|
|
2952
2937
|
/>
|
|
2953
2938
|
);
|
|
@@ -644,6 +644,326 @@ describe('useValidation', () => {
|
|
|
644
644
|
// The functionality is verified through integration tests and the hasErrors test above
|
|
645
645
|
});
|
|
646
646
|
|
|
647
|
+
describe('getLineAndColumnFromPosition utility (lines 36-46)', () => {
|
|
648
|
+
it('computes correct line and column from position in security issue', async () => {
|
|
649
|
+
const { isContentSafe, findUnsafeContent } = require('../../utils/contentSanitizer');
|
|
650
|
+
const { validateHTML, extractAndValidateCSS } = require('../../utils/htmlValidator');
|
|
651
|
+
let validationState;
|
|
652
|
+
|
|
653
|
+
validateHTML.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
|
|
654
|
+
extractAndValidateCSS.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
|
|
655
|
+
isContentSafe.mockImplementationOnce(() => false);
|
|
656
|
+
// position 12 in "line1\nline2\nX" → line 3, column 1
|
|
657
|
+
const content = 'line1\nline2\njavascript:alert(1)';
|
|
658
|
+
findUnsafeContent.mockImplementationOnce(() => [{ type: 'JavaScript Protocol', position: 12 }]);
|
|
659
|
+
|
|
660
|
+
render(
|
|
661
|
+
<TestComponent
|
|
662
|
+
content={content}
|
|
663
|
+
options={{ enableRealTime: false }}
|
|
664
|
+
onStateChange={(state) => { validationState = state; }}
|
|
665
|
+
/>
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
await waitFor(() => { expect(validationState).toBeDefined(); });
|
|
669
|
+
await act(async () => { validationState.forceValidation(); await Promise.resolve(); });
|
|
670
|
+
|
|
671
|
+
await waitFor(() => {
|
|
672
|
+
const issues = validationState.getAllIssues();
|
|
673
|
+
const secIssue = issues.find((i) => i.source === 'security');
|
|
674
|
+
expect(secIssue).toBeDefined();
|
|
675
|
+
expect(secIssue.line).toBe(3);
|
|
676
|
+
expect(secIssue.column).toBe(1);
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it('defaults line/column to 1 when security issue has no position', async () => {
|
|
681
|
+
const { isContentSafe, findUnsafeContent } = require('../../utils/contentSanitizer');
|
|
682
|
+
const { validateHTML, extractAndValidateCSS } = require('../../utils/htmlValidator');
|
|
683
|
+
let validationState;
|
|
684
|
+
|
|
685
|
+
validateHTML.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
|
|
686
|
+
extractAndValidateCSS.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
|
|
687
|
+
isContentSafe.mockImplementationOnce(() => false);
|
|
688
|
+
findUnsafeContent.mockImplementationOnce(() => [{ type: 'JavaScript Protocol' }]); // no position
|
|
689
|
+
|
|
690
|
+
render(
|
|
691
|
+
<TestComponent
|
|
692
|
+
content="<a href='javascript:x'>x</a>"
|
|
693
|
+
options={{ enableRealTime: false }}
|
|
694
|
+
onStateChange={(state) => { validationState = state; }}
|
|
695
|
+
/>
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
await waitFor(() => { expect(validationState).toBeDefined(); });
|
|
699
|
+
await act(async () => { validationState.forceValidation(); await Promise.resolve(); });
|
|
700
|
+
|
|
701
|
+
await waitFor(() => {
|
|
702
|
+
const issues = validationState.getAllIssues();
|
|
703
|
+
const secIssue = issues.find((i) => i.source === 'security');
|
|
704
|
+
expect(secIssue).toBeDefined();
|
|
705
|
+
expect(secIssue.line).toBe(1);
|
|
706
|
+
expect(secIssue.column).toBe(1);
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it('handles negative or undefined position in getLineAndColumnFromPosition', async () => {
|
|
711
|
+
const { isContentSafe, findUnsafeContent } = require('../../utils/contentSanitizer');
|
|
712
|
+
const { validateHTML, extractAndValidateCSS } = require('../../utils/htmlValidator');
|
|
713
|
+
let validationState;
|
|
714
|
+
|
|
715
|
+
validateHTML.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
|
|
716
|
+
extractAndValidateCSS.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
|
|
717
|
+
isContentSafe.mockImplementationOnce(() => false);
|
|
718
|
+
findUnsafeContent.mockImplementationOnce(() => [{ type: 'JavaScript Protocol', position: -5 }]);
|
|
719
|
+
|
|
720
|
+
render(
|
|
721
|
+
<TestComponent
|
|
722
|
+
content="<a href='javascript:x'>x</a>"
|
|
723
|
+
options={{ enableRealTime: false }}
|
|
724
|
+
onStateChange={(state) => { validationState = state; }}
|
|
725
|
+
/>
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
await waitFor(() => { expect(validationState).toBeDefined(); });
|
|
729
|
+
await act(async () => { validationState.forceValidation(); await Promise.resolve(); });
|
|
730
|
+
|
|
731
|
+
await waitFor(() => {
|
|
732
|
+
const issues = validationState.getAllIssues();
|
|
733
|
+
const secIssue = issues.find((i) => i.source === 'security');
|
|
734
|
+
expect(secIssue).toBeDefined();
|
|
735
|
+
// negative position falls back to line 1, column 1
|
|
736
|
+
expect(secIssue.line).toBe(1);
|
|
737
|
+
expect(secIssue.column).toBe(1);
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
describe('getAllIssues spread — all six arrays included (lines 401-411)', () => {
|
|
743
|
+
it('includes items from htmlErrors, htmlWarnings, htmlInfo, cssErrors, cssWarnings, cssInfo', async () => {
|
|
744
|
+
const { validateHTML, extractAndValidateCSS } = require('../../utils/htmlValidator');
|
|
745
|
+
const { isContentSafe, findUnsafeContent } = require('../../utils/contentSanitizer');
|
|
746
|
+
let validationState;
|
|
747
|
+
|
|
748
|
+
validateHTML.mockImplementationOnce(() => ({
|
|
749
|
+
isValid: false,
|
|
750
|
+
errors: [{ type: 'error', message: 'html error', line: 1, column: 1, rule: 'r1', severity: 'error', source: 'htmlhint' }],
|
|
751
|
+
warnings: [{ type: 'warning', message: 'html warning', line: 2, column: 1, rule: 'r2', severity: 'warning', source: 'htmlhint' }],
|
|
752
|
+
info: [{ type: 'info', message: 'html info', line: 3, column: 1, rule: 'r3', severity: 'info', source: 'htmlhint' }],
|
|
753
|
+
}));
|
|
754
|
+
extractAndValidateCSS.mockImplementationOnce(() => ({
|
|
755
|
+
isValid: false,
|
|
756
|
+
errors: [{ type: 'error', message: 'css error', line: 4, column: 1, rule: 'r4', severity: 'error', source: 'css-validator' }],
|
|
757
|
+
warnings: [{ type: 'warning', message: 'css warning', line: 5, column: 1, rule: 'r5', severity: 'warning', source: 'css-validator' }],
|
|
758
|
+
info: [{ type: 'info', message: 'css info', line: 6, column: 1, rule: 'r6', severity: 'info', source: 'css-validator' }],
|
|
759
|
+
}));
|
|
760
|
+
isContentSafe.mockImplementationOnce(() => true);
|
|
761
|
+
findUnsafeContent.mockImplementationOnce(() => []);
|
|
762
|
+
|
|
763
|
+
render(
|
|
764
|
+
<TestComponent
|
|
765
|
+
content="<div>test</div>"
|
|
766
|
+
options={{ enableRealTime: false, enableSanitization: false }}
|
|
767
|
+
onStateChange={(state) => { validationState = state; }}
|
|
768
|
+
/>
|
|
769
|
+
);
|
|
770
|
+
|
|
771
|
+
await waitFor(() => { expect(validationState).toBeDefined(); });
|
|
772
|
+
await act(async () => { validationState.forceValidation(); await Promise.resolve(); });
|
|
773
|
+
|
|
774
|
+
await waitFor(() => {
|
|
775
|
+
const issues = validationState.getAllIssues();
|
|
776
|
+
const rules = issues.map((i) => i.rule);
|
|
777
|
+
expect(rules).toContain('r1'); // htmlErrors
|
|
778
|
+
expect(rules).toContain('r2'); // htmlWarnings
|
|
779
|
+
expect(rules).toContain('r3'); // htmlInfo
|
|
780
|
+
expect(rules).toContain('r4'); // cssErrors
|
|
781
|
+
expect(rules).toContain('r5'); // cssWarnings
|
|
782
|
+
expect(rules).toContain('r6'); // cssInfo
|
|
783
|
+
expect(issues.length).toBeGreaterThanOrEqual(6);
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
it('sorts issues: errors before warnings before info', async () => {
|
|
788
|
+
const { validateHTML, extractAndValidateCSS } = require('../../utils/htmlValidator');
|
|
789
|
+
const { isContentSafe, findUnsafeContent } = require('../../utils/contentSanitizer');
|
|
790
|
+
let validationState;
|
|
791
|
+
|
|
792
|
+
validateHTML.mockImplementationOnce(() => ({
|
|
793
|
+
isValid: false,
|
|
794
|
+
errors: [{ type: 'error', message: 'html error', line: 10, column: 1, rule: 're', severity: 'error', source: 'htmlhint' }],
|
|
795
|
+
warnings: [{ type: 'warning', message: 'html warning', line: 1, column: 1, rule: 'rw', severity: 'warning', source: 'htmlhint' }],
|
|
796
|
+
info: [{ type: 'info', message: 'html info', line: 1, column: 1, rule: 'ri', severity: 'info', source: 'htmlhint' }],
|
|
797
|
+
}));
|
|
798
|
+
extractAndValidateCSS.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
|
|
799
|
+
isContentSafe.mockImplementationOnce(() => true);
|
|
800
|
+
findUnsafeContent.mockImplementationOnce(() => []);
|
|
801
|
+
|
|
802
|
+
render(
|
|
803
|
+
<TestComponent
|
|
804
|
+
content="<div>test</div>"
|
|
805
|
+
options={{ enableRealTime: false, enableSanitization: false }}
|
|
806
|
+
onStateChange={(state) => { validationState = state; }}
|
|
807
|
+
/>
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
await waitFor(() => { expect(validationState).toBeDefined(); });
|
|
811
|
+
await act(async () => { validationState.forceValidation(); await Promise.resolve(); });
|
|
812
|
+
|
|
813
|
+
await waitFor(() => {
|
|
814
|
+
const issues = validationState.getAllIssues();
|
|
815
|
+
const severities = issues.map((i) => i.severity);
|
|
816
|
+
// Errors should come before warnings, warnings before info
|
|
817
|
+
const firstError = severities.indexOf('error');
|
|
818
|
+
const firstWarning = severities.indexOf('warning');
|
|
819
|
+
const firstInfo = severities.indexOf('info');
|
|
820
|
+
if (firstError !== -1 && firstWarning !== -1) expect(firstError).toBeLessThan(firstWarning);
|
|
821
|
+
if (firstWarning !== -1 && firstInfo !== -1) expect(firstWarning).toBeLessThan(firstInfo);
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
describe('hasClientSideLiquidErrors and hasBlockingErrors (lines 455-456)', () => {
|
|
827
|
+
it('sets hasBlockingErrors=true when htmlErrors has a liquid-validator error', async () => {
|
|
828
|
+
const { validateHTML, extractAndValidateCSS } = require('../../utils/htmlValidator');
|
|
829
|
+
const { isContentSafe, findUnsafeContent } = require('../../utils/contentSanitizer');
|
|
830
|
+
let validationState;
|
|
831
|
+
|
|
832
|
+
validateHTML.mockImplementationOnce(() => ({
|
|
833
|
+
isValid: false,
|
|
834
|
+
errors: [{
|
|
835
|
+
type: 'error',
|
|
836
|
+
message: 'Liquid syntax error',
|
|
837
|
+
line: 1,
|
|
838
|
+
column: 1,
|
|
839
|
+
rule: 'liquid-syntax',
|
|
840
|
+
severity: 'error',
|
|
841
|
+
source: 'liquid-validator', // ISSUE_SOURCES.LIQUID
|
|
842
|
+
}],
|
|
843
|
+
warnings: [],
|
|
844
|
+
info: [],
|
|
845
|
+
}));
|
|
846
|
+
extractAndValidateCSS.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
|
|
847
|
+
isContentSafe.mockImplementationOnce(() => true);
|
|
848
|
+
findUnsafeContent.mockImplementationOnce(() => []);
|
|
849
|
+
|
|
850
|
+
render(
|
|
851
|
+
<TestComponent
|
|
852
|
+
content="{{ invalid liquid }}"
|
|
853
|
+
options={{ enableRealTime: false, enableSanitization: false }}
|
|
854
|
+
onStateChange={(state) => { validationState = state; }}
|
|
855
|
+
/>
|
|
856
|
+
);
|
|
857
|
+
|
|
858
|
+
await waitFor(() => { expect(validationState).toBeDefined(); });
|
|
859
|
+
await act(async () => { validationState.forceValidation(); await Promise.resolve(); });
|
|
860
|
+
|
|
861
|
+
await waitFor(() => {
|
|
862
|
+
// hasClientSideLiquidErrors → true because htmlErrors has liquid-validator+error item
|
|
863
|
+
// therefore hasBlockingErrors → true
|
|
864
|
+
expect(validationState.hasBlockingErrors).toBe(true);
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
it('does NOT set hasBlockingErrors from liquid-validator warning (non-error severity)', async () => {
|
|
869
|
+
const { validateHTML, extractAndValidateCSS } = require('../../utils/htmlValidator');
|
|
870
|
+
const { isContentSafe, findUnsafeContent } = require('../../utils/contentSanitizer');
|
|
871
|
+
let validationState;
|
|
872
|
+
|
|
873
|
+
validateHTML.mockImplementationOnce(() => ({
|
|
874
|
+
isValid: true,
|
|
875
|
+
errors: [],
|
|
876
|
+
warnings: [{
|
|
877
|
+
type: 'warning',
|
|
878
|
+
message: 'Liquid warning',
|
|
879
|
+
line: 1,
|
|
880
|
+
column: 1,
|
|
881
|
+
rule: 'liquid-warning',
|
|
882
|
+
severity: 'warning',
|
|
883
|
+
source: 'liquid-validator',
|
|
884
|
+
}],
|
|
885
|
+
info: [],
|
|
886
|
+
}));
|
|
887
|
+
extractAndValidateCSS.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
|
|
888
|
+
isContentSafe.mockImplementationOnce(() => true);
|
|
889
|
+
findUnsafeContent.mockImplementationOnce(() => []);
|
|
890
|
+
|
|
891
|
+
render(
|
|
892
|
+
<TestComponent
|
|
893
|
+
content="<div>test</div>"
|
|
894
|
+
options={{ enableRealTime: false, enableSanitization: false }}
|
|
895
|
+
onStateChange={(state) => { validationState = state; }}
|
|
896
|
+
/>
|
|
897
|
+
);
|
|
898
|
+
|
|
899
|
+
await waitFor(() => { expect(validationState).toBeDefined(); });
|
|
900
|
+
await act(async () => { validationState.forceValidation(); await Promise.resolve(); });
|
|
901
|
+
|
|
902
|
+
await waitFor(() => {
|
|
903
|
+
// hasClientSideLiquidErrors → false (severity is warning, not error)
|
|
904
|
+
// No other blocking conditions → hasBlockingErrors → false
|
|
905
|
+
expect(validationState.hasBlockingErrors).toBe(false);
|
|
906
|
+
});
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
it('sets hasBlockingErrors=true via API liquid errors', async () => {
|
|
910
|
+
const { validateHTML, extractAndValidateCSS } = require('../../utils/htmlValidator');
|
|
911
|
+
const { isContentSafe, findUnsafeContent } = require('../../utils/contentSanitizer');
|
|
912
|
+
let validationState;
|
|
913
|
+
|
|
914
|
+
validateHTML.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
|
|
915
|
+
extractAndValidateCSS.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
|
|
916
|
+
isContentSafe.mockImplementationOnce(() => true);
|
|
917
|
+
findUnsafeContent.mockImplementationOnce(() => []);
|
|
918
|
+
|
|
919
|
+
render(
|
|
920
|
+
<TestComponent
|
|
921
|
+
content="<div>test</div>"
|
|
922
|
+
options={{
|
|
923
|
+
enableRealTime: false,
|
|
924
|
+
enableSanitization: false,
|
|
925
|
+
apiValidationErrors: { liquidErrors: ['API liquid error'], standardErrors: [] },
|
|
926
|
+
}}
|
|
927
|
+
onStateChange={(state) => { validationState = state; }}
|
|
928
|
+
/>
|
|
929
|
+
);
|
|
930
|
+
|
|
931
|
+
await waitFor(() => { expect(validationState).toBeDefined(); });
|
|
932
|
+
await act(async () => { validationState.forceValidation(); await Promise.resolve(); });
|
|
933
|
+
|
|
934
|
+
await waitFor(() => {
|
|
935
|
+
// hasApiErrors → true → hasBlockingErrors → true
|
|
936
|
+
expect(validationState.hasBlockingErrors).toBe(true);
|
|
937
|
+
});
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
it('hasBlockingErrors=false when no blocking conditions are present', async () => {
|
|
941
|
+
const { validateHTML, extractAndValidateCSS } = require('../../utils/htmlValidator');
|
|
942
|
+
const { isContentSafe, findUnsafeContent } = require('../../utils/contentSanitizer');
|
|
943
|
+
let validationState;
|
|
944
|
+
|
|
945
|
+
validateHTML.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
|
|
946
|
+
extractAndValidateCSS.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
|
|
947
|
+
isContentSafe.mockImplementationOnce(() => true);
|
|
948
|
+
findUnsafeContent.mockImplementationOnce(() => []);
|
|
949
|
+
|
|
950
|
+
render(
|
|
951
|
+
<TestComponent
|
|
952
|
+
content="<div>clean</div>"
|
|
953
|
+
options={{ enableRealTime: false, enableSanitization: false }}
|
|
954
|
+
onStateChange={(state) => { validationState = state; }}
|
|
955
|
+
/>
|
|
956
|
+
);
|
|
957
|
+
|
|
958
|
+
await waitFor(() => { expect(validationState).toBeDefined(); });
|
|
959
|
+
await act(async () => { validationState.forceValidation(); await Promise.resolve(); });
|
|
960
|
+
|
|
961
|
+
await waitFor(() => {
|
|
962
|
+
expect(validationState.hasBlockingErrors).toBe(false);
|
|
963
|
+
});
|
|
964
|
+
});
|
|
965
|
+
});
|
|
966
|
+
|
|
647
967
|
describe('Blocking errors', () => {
|
|
648
968
|
it('treats protocol security issues as blocking errors', async () => {
|
|
649
969
|
const { isContentSafe, findUnsafeContent } = require('../../utils/contentSanitizer');
|