@capillarytech/creatives-library 8.0.326 → 8.0.327
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/utils/tests/tagValidations.test.js +34 -0
- package/v2Components/CapTagList/index.js +14 -22
- package/v2Components/CapTagList/style.scss +48 -0
- package/v2Components/CapTagListWithInput/__tests__/CapTagListWithInput.test.js +63 -0
- package/v2Components/CapTagListWithInput/index.js +4 -0
- package/v2Components/CapWhatsappCTA/index.js +2 -0
- package/v2Components/FormBuilder/index.js +7 -0
- package/v2Components/HtmlEditor/HTMLEditor.js +6 -1
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -0
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +927 -2
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +3 -0
- package/v2Containers/BeeEditor/index.js +3 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +28 -1
- package/v2Containers/CreativesContainer/index.js +3 -0
- package/v2Containers/Email/index.js +1 -0
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +7 -1
- package/v2Containers/EmailWrapper/components/EmailWrapperView.js +3 -0
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +20 -2
- package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +16 -1
- package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +3 -0
- package/v2Containers/EmailWrapper/index.js +4 -0
- package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +1 -0
- package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +9 -0
- package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +19 -0
- package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +3 -0
- package/v2Containers/InAppWrapper/index.js +3 -0
- package/v2Containers/MobilePush/Create/index.js +2 -0
- package/v2Containers/MobilePush/Edit/index.js +2 -0
- package/v2Containers/MobilepushWrapper/index.js +3 -1
- package/v2Containers/Rcs/index.js +1 -0
- package/v2Containers/Sms/Create/index.js +2 -0
- package/v2Containers/Sms/Edit/index.js +2 -0
- package/v2Containers/SmsTrai/Edit/index.js +2 -0
- package/v2Containers/SmsWrapper/index.js +2 -0
- package/v2Containers/TagList/index.js +41 -2
- package/v2Containers/TagList/messages.js +4 -0
- package/v2Containers/TagList/tests/TagList.test.js +122 -20
- package/v2Containers/TagList/tests/mockdata.js +17 -0
- package/v2Containers/Viber/index.js +5 -0
- package/v2Containers/WebPush/Create/hooks/useTagManagement.js +0 -2
- package/v2Containers/WebPush/Create/index.js +9 -1
- package/v2Containers/Whatsapp/index.js +5 -0
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +20 -0
- package/v2Containers/Zalo/index.js +2 -0
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
import '@testing-library/jest-dom';
|
|
12
12
|
import { IntlProvider } from 'react-intl';
|
|
13
13
|
import HTMLEditor from '../HTMLEditor';
|
|
14
|
+
import { VALIDATION_SEVERITY } from '../constants';
|
|
15
|
+
import { ISSUE_SOURCES } from '../utils/validationConstants';
|
|
14
16
|
|
|
15
17
|
// Options to control CodeEditorPane mock behavior
|
|
16
18
|
const mockCodeEditorOptions = {
|
|
@@ -19,6 +21,7 @@ const mockCodeEditorOptions = {
|
|
|
19
21
|
setRef: true,
|
|
20
22
|
navigateToLineThrows: false,
|
|
21
23
|
includeNavigateToLine: true,
|
|
24
|
+
omitGetCursor: false,
|
|
22
25
|
};
|
|
23
26
|
|
|
24
27
|
|
|
@@ -166,11 +169,14 @@ jest.mock('../components/CodeEditorPane', () => {
|
|
|
166
169
|
|
|
167
170
|
const methods = {
|
|
168
171
|
focus: jest.fn(),
|
|
169
|
-
getCursor: jest.fn(() => 0),
|
|
170
172
|
getValue: jest.fn(() => value),
|
|
171
173
|
setValue: jest.fn((newValue) => setValue(newValue)),
|
|
172
174
|
};
|
|
173
175
|
|
|
176
|
+
if (!mockCodeEditorOptions.omitGetCursor) {
|
|
177
|
+
methods.getCursor = jest.fn(() => 0);
|
|
178
|
+
}
|
|
179
|
+
|
|
174
180
|
if (mockCodeEditorOptions.includeNavigateToLine) {
|
|
175
181
|
methods.navigateToLine = jest.fn(() => {
|
|
176
182
|
if (mockCodeEditorOptions.navigateToLineThrows) {
|
|
@@ -199,7 +205,20 @@ jest.mock('../components/CodeEditorPane', () => {
|
|
|
199
205
|
}
|
|
200
206
|
|
|
201
207
|
return (
|
|
202
|
-
<div
|
|
208
|
+
<div
|
|
209
|
+
data-testid="code-editor-pane"
|
|
210
|
+
data-tags={JSON.stringify(props.tags ?? null)}
|
|
211
|
+
data-injected-tags={JSON.stringify(props.injectedTags ?? null)}
|
|
212
|
+
data-location={JSON.stringify(props.location ?? null)}
|
|
213
|
+
data-event-context-tags={JSON.stringify(props.eventContextTags ?? null)}
|
|
214
|
+
data-wait-event-context-tags={JSON.stringify(props.waitEventContextTags ?? null)}
|
|
215
|
+
data-selected-offer-details={JSON.stringify(props.selectedOfferDetails ?? null)}
|
|
216
|
+
data-channel={JSON.stringify(props.channel ?? null)}
|
|
217
|
+
data-user-locale={props.userLocale ?? ''}
|
|
218
|
+
data-module-filter-enabled={String(!!props.moduleFilterEnabled)}
|
|
219
|
+
data-read-only={String(!!props.readOnly)}
|
|
220
|
+
data-is-fullscreen-mode={String(!!props.isFullscreenMode)}
|
|
221
|
+
>
|
|
203
222
|
<textarea
|
|
204
223
|
value={value}
|
|
205
224
|
onChange={(e) => {
|
|
@@ -216,6 +235,18 @@ jest.mock('../components/CodeEditorPane', () => {
|
|
|
216
235
|
>
|
|
217
236
|
Trigger Context Change
|
|
218
237
|
</button>
|
|
238
|
+
<button
|
|
239
|
+
onClick={() => props.onContextChange && props.onContextChange('ALL')}
|
|
240
|
+
data-testid="trigger-context-all"
|
|
241
|
+
>
|
|
242
|
+
Trigger Context ALL
|
|
243
|
+
</button>
|
|
244
|
+
<button
|
|
245
|
+
onClick={() => props.onContextChange && props.onContextChange(null)}
|
|
246
|
+
data-testid="trigger-context-null"
|
|
247
|
+
>
|
|
248
|
+
Trigger Context Null
|
|
249
|
+
</button>
|
|
219
250
|
{validation && (
|
|
220
251
|
<ValidationErrorDisplay
|
|
221
252
|
validation={validation}
|
|
@@ -249,6 +280,9 @@ jest.mock('../components/ValidationErrorDisplay', () => function MockValidationE
|
|
|
249
280
|
<button onClick={() => onErrorClick && onErrorClick({ line: null })}>
|
|
250
281
|
Error without Line
|
|
251
282
|
</button>
|
|
283
|
+
<button onClick={() => onErrorClick && onErrorClick(undefined)}>
|
|
284
|
+
Error undefined
|
|
285
|
+
</button>
|
|
252
286
|
</div>
|
|
253
287
|
);
|
|
254
288
|
});
|
|
@@ -384,6 +418,195 @@ describe('HTMLEditor', () => {
|
|
|
384
418
|
});
|
|
385
419
|
});
|
|
386
420
|
|
|
421
|
+
describe('waitEventContextTags', () => {
|
|
422
|
+
const waitTags = {
|
|
423
|
+
blockA: {
|
|
424
|
+
eventName: 'Order Placed',
|
|
425
|
+
blockName: 'Wait',
|
|
426
|
+
tags: [{ tagName: 'wait.foo', label: 'Foo', profileId: 'p1', profileName: 'P1' }],
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
it('passes default empty object to CodeEditorPane when waitEventContextTags is omitted', () => {
|
|
431
|
+
render(
|
|
432
|
+
<TestWrapper>
|
|
433
|
+
<HTMLEditor {...defaultProps} />
|
|
434
|
+
</TestWrapper>
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
act(() => {
|
|
438
|
+
jest.runAllTimers();
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const pane = screen.getByTestId('code-editor-pane');
|
|
442
|
+
expect(pane).toHaveAttribute('data-wait-event-context-tags', JSON.stringify({}));
|
|
443
|
+
expect(pane).toHaveAttribute('data-is-fullscreen-mode', 'false');
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('forwards custom waitEventContextTags to the main CodeEditorPane', () => {
|
|
447
|
+
render(
|
|
448
|
+
<TestWrapper>
|
|
449
|
+
<HTMLEditor {...defaultProps} waitEventContextTags={waitTags} />
|
|
450
|
+
</TestWrapper>
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
act(() => {
|
|
454
|
+
jest.runAllTimers();
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const pane = screen.getByTestId('code-editor-pane');
|
|
458
|
+
expect(pane).toHaveAttribute('data-wait-event-context-tags', JSON.stringify(waitTags));
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('forwards the same waitEventContextTags to fullscreen CodeEditorPane when modal opens', () => {
|
|
462
|
+
render(
|
|
463
|
+
<TestWrapper>
|
|
464
|
+
<HTMLEditor {...defaultProps} waitEventContextTags={waitTags} />
|
|
465
|
+
</TestWrapper>
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
act(() => {
|
|
469
|
+
jest.runAllTimers();
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
fireEvent.click(screen.getByText('Toggle Fullscreen'));
|
|
473
|
+
|
|
474
|
+
act(() => {
|
|
475
|
+
jest.runAllTimers();
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
const panes = screen.getAllByTestId('code-editor-pane');
|
|
479
|
+
expect(panes.length).toBeGreaterThanOrEqual(2);
|
|
480
|
+
const expected = JSON.stringify(waitTags);
|
|
481
|
+
panes.forEach((pane) => {
|
|
482
|
+
expect(pane).toHaveAttribute('data-wait-event-context-tags', expected);
|
|
483
|
+
});
|
|
484
|
+
const mainPane = panes.find((el) => el.getAttribute('data-is-fullscreen-mode') === 'false');
|
|
485
|
+
const fsPane = panes.find((el) => el.getAttribute('data-is-fullscreen-mode') === 'true');
|
|
486
|
+
expect(mainPane).toBeTruthy();
|
|
487
|
+
expect(fsPane).toBeTruthy();
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('forwards waitEventContextTags for inapp variant to main and fullscreen CodeEditorPane', () => {
|
|
491
|
+
render(
|
|
492
|
+
<TestWrapper>
|
|
493
|
+
<HTMLEditor {...defaultProps} variant="inapp" waitEventContextTags={waitTags} />
|
|
494
|
+
</TestWrapper>
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
act(() => {
|
|
498
|
+
jest.runAllTimers();
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
expect(screen.getByTestId('code-editor-pane')).toHaveAttribute(
|
|
502
|
+
'data-wait-event-context-tags',
|
|
503
|
+
JSON.stringify(waitTags),
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
fireEvent.click(screen.getByText('Toggle Fullscreen'));
|
|
507
|
+
|
|
508
|
+
act(() => {
|
|
509
|
+
jest.runAllTimers();
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const panes = screen.getAllByTestId('code-editor-pane');
|
|
513
|
+
const expected = JSON.stringify(waitTags);
|
|
514
|
+
panes.forEach((pane) => {
|
|
515
|
+
expect(pane).toHaveAttribute('data-wait-event-context-tags', expected);
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('passes explicit null waitEventContextTags through to CodeEditorPane', () => {
|
|
520
|
+
render(
|
|
521
|
+
<TestWrapper>
|
|
522
|
+
<HTMLEditor {...defaultProps} waitEventContextTags={null} />
|
|
523
|
+
</TestWrapper>
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
act(() => {
|
|
527
|
+
jest.runAllTimers();
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
expect(screen.getByTestId('code-editor-pane')).toHaveAttribute(
|
|
531
|
+
'data-wait-event-context-tags',
|
|
532
|
+
JSON.stringify(null),
|
|
533
|
+
);
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Covers forwardRef destructuring / defaults for tag-related props (HTMLEditor.js ~93–101)
|
|
539
|
+
* merged with HTMLEditor.defaultProps and passed through to CodeEditorPane (~663–671).
|
|
540
|
+
*/
|
|
541
|
+
describe('forwardRef tag-related props (HTMLEditor.js lines 93–101 → CodeEditorPane)', () => {
|
|
542
|
+
it('passes merged default tag-related props to CodeEditorPane when omitted on the instance', () => {
|
|
543
|
+
render(
|
|
544
|
+
<TestWrapper>
|
|
545
|
+
<HTMLEditor {...defaultProps} />
|
|
546
|
+
</TestWrapper>
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
act(() => {
|
|
550
|
+
jest.runAllTimers();
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
const pane = screen.getByTestId('code-editor-pane');
|
|
554
|
+
expect(pane).toHaveAttribute('data-tags', JSON.stringify([]));
|
|
555
|
+
expect(pane).toHaveAttribute('data-injected-tags', JSON.stringify({}));
|
|
556
|
+
expect(pane).toHaveAttribute('data-location', JSON.stringify(null));
|
|
557
|
+
expect(pane).toHaveAttribute('data-event-context-tags', JSON.stringify([]));
|
|
558
|
+
expect(pane).toHaveAttribute('data-wait-event-context-tags', JSON.stringify({}));
|
|
559
|
+
expect(pane).toHaveAttribute('data-selected-offer-details', JSON.stringify([]));
|
|
560
|
+
expect(pane).toHaveAttribute('data-channel', JSON.stringify(null));
|
|
561
|
+
expect(pane).toHaveAttribute('data-user-locale', 'en');
|
|
562
|
+
expect(pane).toHaveAttribute('data-module-filter-enabled', 'true');
|
|
563
|
+
expect(pane).toHaveAttribute('data-read-only', 'false');
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('forwards explicit tag-related props from the forwardRef parameter list to CodeEditorPane', () => {
|
|
567
|
+
const tags = [{ id: 't1' }];
|
|
568
|
+
const injectedTags = { custom: { name: 'Custom' } };
|
|
569
|
+
const location = { query: { module: 'outbound' } };
|
|
570
|
+
const eventContextTags = [{ tagName: 'entryTrigger.x' }];
|
|
571
|
+
const waitEventContextTags = { block1: { eventName: 'E', blockName: 'B', tags: [] } };
|
|
572
|
+
const selectedOfferDetails = [{ type: 'coupon' }];
|
|
573
|
+
|
|
574
|
+
render(
|
|
575
|
+
<TestWrapper>
|
|
576
|
+
<HTMLEditor
|
|
577
|
+
{...defaultProps}
|
|
578
|
+
tags={tags}
|
|
579
|
+
injectedTags={injectedTags}
|
|
580
|
+
location={location}
|
|
581
|
+
eventContextTags={eventContextTags}
|
|
582
|
+
waitEventContextTags={waitEventContextTags}
|
|
583
|
+
selectedOfferDetails={selectedOfferDetails}
|
|
584
|
+
channel="SMS"
|
|
585
|
+
userLocale="ja"
|
|
586
|
+
moduleFilterEnabled={false}
|
|
587
|
+
readOnly
|
|
588
|
+
/>
|
|
589
|
+
</TestWrapper>
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
act(() => {
|
|
593
|
+
jest.runAllTimers();
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
const pane = screen.getByTestId('code-editor-pane');
|
|
597
|
+
expect(pane).toHaveAttribute('data-tags', JSON.stringify(tags));
|
|
598
|
+
expect(pane).toHaveAttribute('data-injected-tags', JSON.stringify(injectedTags));
|
|
599
|
+
expect(pane).toHaveAttribute('data-location', JSON.stringify(location));
|
|
600
|
+
expect(pane).toHaveAttribute('data-event-context-tags', JSON.stringify(eventContextTags));
|
|
601
|
+
expect(pane).toHaveAttribute('data-wait-event-context-tags', JSON.stringify(waitEventContextTags));
|
|
602
|
+
expect(pane).toHaveAttribute('data-selected-offer-details', JSON.stringify(selectedOfferDetails));
|
|
603
|
+
expect(pane).toHaveAttribute('data-channel', JSON.stringify('SMS'));
|
|
604
|
+
expect(pane).toHaveAttribute('data-user-locale', 'ja');
|
|
605
|
+
expect(pane).toHaveAttribute('data-module-filter-enabled', 'false');
|
|
606
|
+
expect(pane).toHaveAttribute('data-read-only', 'true');
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
|
|
387
610
|
describe('Context Provider', () => {
|
|
388
611
|
it('provides correct context value for email variant', () => {
|
|
389
612
|
render(
|
|
@@ -2479,6 +2702,7 @@ describe('HTMLEditor', () => {
|
|
|
2479
2702
|
mockCodeEditorOptions.setRef = true;
|
|
2480
2703
|
mockCodeEditorOptions.navigateToLineThrows = false;
|
|
2481
2704
|
mockCodeEditorOptions.includeNavigateToLine = true;
|
|
2705
|
+
mockCodeEditorOptions.omitGetCursor = false;
|
|
2482
2706
|
|
|
2483
2707
|
// Clear specific mocks instead of all to avoid breaking other mocks
|
|
2484
2708
|
CapNotification.warning.mockClear();
|
|
@@ -3467,6 +3691,7 @@ describe('HTMLEditor', () => {
|
|
|
3467
3691
|
tags={null}
|
|
3468
3692
|
injectedTags={null}
|
|
3469
3693
|
eventContextTags={null}
|
|
3694
|
+
waitEventContextTags={null}
|
|
3470
3695
|
selectedOfferDetails={null}
|
|
3471
3696
|
onTagContextChange={null}
|
|
3472
3697
|
onTagSelect={null}
|
|
@@ -3492,6 +3717,7 @@ describe('HTMLEditor', () => {
|
|
|
3492
3717
|
<HTMLEditor
|
|
3493
3718
|
tags={[]}
|
|
3494
3719
|
eventContextTags={[]}
|
|
3720
|
+
waitEventContextTags={{}}
|
|
3495
3721
|
selectedOfferDetails={[]}
|
|
3496
3722
|
/>
|
|
3497
3723
|
</TestWrapper>
|
|
@@ -3541,4 +3767,703 @@ describe('HTMLEditor', () => {
|
|
|
3541
3767
|
expect(screen.getByTestId('editor-toolbar')).toBeInTheDocument();
|
|
3542
3768
|
});
|
|
3543
3769
|
});
|
|
3770
|
+
|
|
3771
|
+
/**
|
|
3772
|
+
* Covers remaining HTMLEditor.js branches for 100% file coverage:
|
|
3773
|
+
* useImperativeHandle getters (302–304), validation effects (350, 416, 440–441), onErrorAcknowledged (543).
|
|
3774
|
+
*/
|
|
3775
|
+
describe('HTMLEditor full file coverage', () => {
|
|
3776
|
+
beforeEach(() => {
|
|
3777
|
+
mockCodeEditorOptions.omitGetCursor = false;
|
|
3778
|
+
mockCodeEditorOptions.setRef = true;
|
|
3779
|
+
mockCodeEditorOptions.includeInsertText = true;
|
|
3780
|
+
mockCodeEditorOptions.insertTextThrows = false;
|
|
3781
|
+
|
|
3782
|
+
mockUseValidationImpl.mockReset();
|
|
3783
|
+
mockUseValidationImpl.mockImplementation(() => ({
|
|
3784
|
+
isValidating: false,
|
|
3785
|
+
getAllIssues: () => [],
|
|
3786
|
+
isClean: () => true,
|
|
3787
|
+
summary: { totalErrors: 0, totalWarnings: 0 },
|
|
3788
|
+
}));
|
|
3789
|
+
|
|
3790
|
+
require('../hooks/useEditorContent').useEditorContent = () => ({
|
|
3791
|
+
content: '<p>Test content</p>',
|
|
3792
|
+
updateContent: jest.fn(),
|
|
3793
|
+
saveContent: jest.fn(),
|
|
3794
|
+
markAsSaved: jest.fn(),
|
|
3795
|
+
isLoading: false,
|
|
3796
|
+
isDirty: false,
|
|
3797
|
+
hasContent: true,
|
|
3798
|
+
getContentSize: jest.fn(() => 20),
|
|
3799
|
+
});
|
|
3800
|
+
|
|
3801
|
+
require('../hooks/useInAppContent').useInAppContent = () => ({
|
|
3802
|
+
content: '<p>Android content</p>',
|
|
3803
|
+
deviceContent: {
|
|
3804
|
+
android: '<p>Android content</p>',
|
|
3805
|
+
ios: '<p>iOS content</p>',
|
|
3806
|
+
},
|
|
3807
|
+
activeDevice: 'android',
|
|
3808
|
+
keepContentSame: false,
|
|
3809
|
+
updateContent: jest.fn(),
|
|
3810
|
+
saveContent: jest.fn(),
|
|
3811
|
+
markAsSaved: jest.fn(),
|
|
3812
|
+
switchDevice: jest.fn(),
|
|
3813
|
+
toggleContentSync: jest.fn(),
|
|
3814
|
+
getDeviceContent: (device) => `<p>${device} content</p>`,
|
|
3815
|
+
setDeviceContent: jest.fn(),
|
|
3816
|
+
getContentSize: () => 20,
|
|
3817
|
+
isLoading: false,
|
|
3818
|
+
isDirty: false,
|
|
3819
|
+
hasContent: true,
|
|
3820
|
+
});
|
|
3821
|
+
});
|
|
3822
|
+
|
|
3823
|
+
it('ref exposes getValidation, getContent, and isContentEmpty (lines 302–304)', async () => {
|
|
3824
|
+
const ref = React.createRef();
|
|
3825
|
+
const WrappedHTMLEditor = HTMLEditor.WrappedComponent || HTMLEditor;
|
|
3826
|
+
const intlProvider = new IntlProvider({ locale: 'en', messages: {} }, {});
|
|
3827
|
+
const { intl } = intlProvider.getChildContext();
|
|
3828
|
+
|
|
3829
|
+
mockUseValidationImpl.mockReturnValue({
|
|
3830
|
+
isValidating: false,
|
|
3831
|
+
getAllIssues: () => [],
|
|
3832
|
+
isClean: () => true,
|
|
3833
|
+
summary: { totalErrors: 0, totalWarnings: 0 },
|
|
3834
|
+
});
|
|
3835
|
+
|
|
3836
|
+
render(
|
|
3837
|
+
<TestWrapper>
|
|
3838
|
+
<WrappedHTMLEditor
|
|
3839
|
+
{...defaultProps}
|
|
3840
|
+
intl={intl}
|
|
3841
|
+
ref={ref}
|
|
3842
|
+
initialContent="<p>hi</p>"
|
|
3843
|
+
/>
|
|
3844
|
+
</TestWrapper>
|
|
3845
|
+
);
|
|
3846
|
+
|
|
3847
|
+
act(() => {
|
|
3848
|
+
jest.runAllTimers();
|
|
3849
|
+
});
|
|
3850
|
+
|
|
3851
|
+
await waitFor(() => {
|
|
3852
|
+
expect(ref.current).toBeTruthy();
|
|
3853
|
+
});
|
|
3854
|
+
|
|
3855
|
+
expect(ref.current.getValidation()).toBeDefined();
|
|
3856
|
+
expect(ref.current.getContent()).toBe('<p>Test content</p>');
|
|
3857
|
+
expect(ref.current.isContentEmpty()).toBe(false);
|
|
3858
|
+
});
|
|
3859
|
+
|
|
3860
|
+
it('skips main validation notify effect while isValidating is true (line 350)', () => {
|
|
3861
|
+
const onValidationChange = jest.fn();
|
|
3862
|
+
mockUseValidationImpl.mockReturnValue({
|
|
3863
|
+
isValidating: true,
|
|
3864
|
+
hasBlockingErrors: false,
|
|
3865
|
+
getAllIssues: () => [],
|
|
3866
|
+
isClean: () => true,
|
|
3867
|
+
summary: { totalErrors: 0, totalWarnings: 0 },
|
|
3868
|
+
});
|
|
3869
|
+
|
|
3870
|
+
render(
|
|
3871
|
+
<TestWrapper>
|
|
3872
|
+
<HTMLEditor {...defaultProps} onValidationChange={onValidationChange} />
|
|
3873
|
+
</TestWrapper>
|
|
3874
|
+
);
|
|
3875
|
+
|
|
3876
|
+
act(() => {
|
|
3877
|
+
jest.runAllTimers();
|
|
3878
|
+
});
|
|
3879
|
+
|
|
3880
|
+
expect(onValidationChange).toHaveBeenCalled();
|
|
3881
|
+
const withComplete = onValidationChange.mock.calls.filter((c) => c[0]?.validationComplete === true);
|
|
3882
|
+
expect(withComplete.length).toBe(0);
|
|
3883
|
+
});
|
|
3884
|
+
|
|
3885
|
+
it('skips api validation errors effect while validation is still running (line 416)', () => {
|
|
3886
|
+
const onValidationChange = jest.fn();
|
|
3887
|
+
mockUseValidationImpl.mockReturnValue({
|
|
3888
|
+
isValidating: true,
|
|
3889
|
+
hasBlockingErrors: false,
|
|
3890
|
+
getAllIssues: () => [],
|
|
3891
|
+
isClean: () => true,
|
|
3892
|
+
summary: { totalErrors: 0, totalWarnings: 0 },
|
|
3893
|
+
});
|
|
3894
|
+
|
|
3895
|
+
render(
|
|
3896
|
+
<TestWrapper>
|
|
3897
|
+
<HTMLEditor
|
|
3898
|
+
{...defaultProps}
|
|
3899
|
+
onValidationChange={onValidationChange}
|
|
3900
|
+
apiValidationErrors={{ liquidErrors: ['x'], standardErrors: [] }}
|
|
3901
|
+
/>
|
|
3902
|
+
</TestWrapper>
|
|
3903
|
+
);
|
|
3904
|
+
|
|
3905
|
+
act(() => {
|
|
3906
|
+
jest.runAllTimers();
|
|
3907
|
+
});
|
|
3908
|
+
|
|
3909
|
+
onValidationChange.mockClear();
|
|
3910
|
+
|
|
3911
|
+
act(() => {
|
|
3912
|
+
jest.runAllTimers();
|
|
3913
|
+
});
|
|
3914
|
+
|
|
3915
|
+
const afterClear = onValidationChange.mock.calls.filter((c) => c[0]?.validationComplete === true);
|
|
3916
|
+
expect(afterClear.length).toBe(0);
|
|
3917
|
+
});
|
|
3918
|
+
|
|
3919
|
+
it('api validation errors effect notifies parent when issue counts change (lines 440–441)', () => {
|
|
3920
|
+
const onValidationChange = jest.fn();
|
|
3921
|
+
let issues = [];
|
|
3922
|
+
|
|
3923
|
+
// Keep summary primitives stable so the main validation effect (line 343) does not re-run
|
|
3924
|
+
// when only getAllIssues() starts returning API-style issues; the api-only effect (409)
|
|
3925
|
+
// still runs because `validation` reference changes each render.
|
|
3926
|
+
mockUseValidationImpl.mockImplementation(() => ({
|
|
3927
|
+
isValidating: false,
|
|
3928
|
+
hasBlockingErrors: false,
|
|
3929
|
+
getAllIssues: () => issues,
|
|
3930
|
+
isClean: () => issues.length === 0,
|
|
3931
|
+
summary: { totalErrors: 0, totalWarnings: 0 },
|
|
3932
|
+
lastValidated: { getTime: () => 0 },
|
|
3933
|
+
}));
|
|
3934
|
+
|
|
3935
|
+
const { rerender } = render(
|
|
3936
|
+
<TestWrapper>
|
|
3937
|
+
<HTMLEditor {...defaultProps} onValidationChange={onValidationChange} />
|
|
3938
|
+
</TestWrapper>
|
|
3939
|
+
);
|
|
3940
|
+
|
|
3941
|
+
act(() => {
|
|
3942
|
+
jest.runAllTimers();
|
|
3943
|
+
});
|
|
3944
|
+
|
|
3945
|
+
onValidationChange.mockClear();
|
|
3946
|
+
|
|
3947
|
+
issues = [{ rule: 'liquid-api-validation', message: 'API error', source: 'liquid' }];
|
|
3948
|
+
|
|
3949
|
+
rerender(
|
|
3950
|
+
<TestWrapper>
|
|
3951
|
+
<HTMLEditor {...defaultProps} onValidationChange={onValidationChange} />
|
|
3952
|
+
</TestWrapper>
|
|
3953
|
+
);
|
|
3954
|
+
|
|
3955
|
+
act(() => {
|
|
3956
|
+
jest.runAllTimers();
|
|
3957
|
+
});
|
|
3958
|
+
|
|
3959
|
+
expect(onValidationChange).toHaveBeenCalled();
|
|
3960
|
+
const lastCall = onValidationChange.mock.calls[onValidationChange.mock.calls.length - 1][0];
|
|
3961
|
+
expect(lastCall.validationComplete).toBe(true);
|
|
3962
|
+
expect(lastCall.issueCounts.errors).toBeGreaterThanOrEqual(1);
|
|
3963
|
+
});
|
|
3964
|
+
|
|
3965
|
+
it('calls onErrorAcknowledged when user clicks a validation error (lines 542–543)', () => {
|
|
3966
|
+
const onErrorAcknowledged = jest.fn();
|
|
3967
|
+
mockUseValidationImpl.mockReturnValue({
|
|
3968
|
+
isValidating: false,
|
|
3969
|
+
getAllIssues: () => [{ source: 'htmlhint', rule: 'rule1', message: 'E' }],
|
|
3970
|
+
isClean: () => false,
|
|
3971
|
+
summary: { totalErrors: 1, totalWarnings: 0 },
|
|
3972
|
+
});
|
|
3973
|
+
|
|
3974
|
+
render(
|
|
3975
|
+
<TestWrapper>
|
|
3976
|
+
<HTMLEditor {...defaultProps} onErrorAcknowledged={onErrorAcknowledged} />
|
|
3977
|
+
</TestWrapper>
|
|
3978
|
+
);
|
|
3979
|
+
|
|
3980
|
+
act(() => {
|
|
3981
|
+
jest.runAllTimers();
|
|
3982
|
+
});
|
|
3983
|
+
|
|
3984
|
+
fireEvent.click(screen.getByText('Error without Line'));
|
|
3985
|
+
expect(onErrorAcknowledged).toHaveBeenCalled();
|
|
3986
|
+
});
|
|
3987
|
+
|
|
3988
|
+
it('getIssueCounts covers isBlockingError branches (standard API, liquid error, sanitizer, warning)', async () => {
|
|
3989
|
+
const ref = React.createRef();
|
|
3990
|
+
const WrappedHTMLEditor = HTMLEditor.WrappedComponent || HTMLEditor;
|
|
3991
|
+
const intlProvider = new IntlProvider({ locale: 'en', messages: {} }, {});
|
|
3992
|
+
const { intl } = intlProvider.getChildContext();
|
|
3993
|
+
|
|
3994
|
+
mockUseValidationImpl.mockReturnValue({
|
|
3995
|
+
isValidating: false,
|
|
3996
|
+
hasBlockingErrors: true,
|
|
3997
|
+
getAllIssues: () => [
|
|
3998
|
+
{ rule: 'standard-api-validation', message: 'a' },
|
|
3999
|
+
{ source: ISSUE_SOURCES.LIQUID, severity: VALIDATION_SEVERITY.ERROR, message: 'b' },
|
|
4000
|
+
{ rule: 'sanitizer.invalidInput', message: 'c' },
|
|
4001
|
+
{ rule: 'html-warn', message: 'd' },
|
|
4002
|
+
],
|
|
4003
|
+
isClean: () => false,
|
|
4004
|
+
summary: { totalErrors: 4, totalWarnings: 0 },
|
|
4005
|
+
});
|
|
4006
|
+
|
|
4007
|
+
render(
|
|
4008
|
+
<TestWrapper>
|
|
4009
|
+
<WrappedHTMLEditor {...defaultProps} intl={intl} ref={ref} />
|
|
4010
|
+
</TestWrapper>
|
|
4011
|
+
);
|
|
4012
|
+
|
|
4013
|
+
act(() => {
|
|
4014
|
+
jest.runAllTimers();
|
|
4015
|
+
});
|
|
4016
|
+
|
|
4017
|
+
await waitFor(() => {
|
|
4018
|
+
expect(ref.current).toBeTruthy();
|
|
4019
|
+
});
|
|
4020
|
+
|
|
4021
|
+
expect(ref.current.getIssueCounts()).toEqual({
|
|
4022
|
+
errors: 3,
|
|
4023
|
+
warnings: 1,
|
|
4024
|
+
total: 4,
|
|
4025
|
+
});
|
|
4026
|
+
});
|
|
4027
|
+
|
|
4028
|
+
it('maps ALL context to DEFAULT in handleContextChange', () => {
|
|
4029
|
+
const globalActions = { fetchSchemaForEntity: jest.fn() };
|
|
4030
|
+
const location = { query: { type: 'full' } };
|
|
4031
|
+
|
|
4032
|
+
render(
|
|
4033
|
+
<TestWrapper>
|
|
4034
|
+
<HTMLEditor
|
|
4035
|
+
{...defaultProps}
|
|
4036
|
+
globalActions={globalActions}
|
|
4037
|
+
location={location}
|
|
4038
|
+
variant="email"
|
|
4039
|
+
/>
|
|
4040
|
+
</TestWrapper>
|
|
4041
|
+
);
|
|
4042
|
+
|
|
4043
|
+
act(() => {
|
|
4044
|
+
jest.runAllTimers();
|
|
4045
|
+
});
|
|
4046
|
+
|
|
4047
|
+
fireEvent.click(screen.getByTestId('trigger-context-all'));
|
|
4048
|
+
|
|
4049
|
+
expect(globalActions.fetchSchemaForEntity).toHaveBeenCalledWith({
|
|
4050
|
+
layout: 'EMAIL',
|
|
4051
|
+
type: 'TAG',
|
|
4052
|
+
context: 'default',
|
|
4053
|
+
embedded: 'full',
|
|
4054
|
+
});
|
|
4055
|
+
});
|
|
4056
|
+
|
|
4057
|
+
it('email variant runs updateContent when initialContent differs from stored content', () => {
|
|
4058
|
+
const updateContent = jest.fn();
|
|
4059
|
+
require('../hooks/useEditorContent').useEditorContent = () => ({
|
|
4060
|
+
content: '<p>Stale</p>',
|
|
4061
|
+
updateContent,
|
|
4062
|
+
saveContent: jest.fn(),
|
|
4063
|
+
markAsSaved: jest.fn(),
|
|
4064
|
+
isLoading: false,
|
|
4065
|
+
isDirty: false,
|
|
4066
|
+
hasContent: true,
|
|
4067
|
+
getContentSize: jest.fn(() => 20),
|
|
4068
|
+
});
|
|
4069
|
+
|
|
4070
|
+
render(
|
|
4071
|
+
<TestWrapper>
|
|
4072
|
+
<HTMLEditor {...defaultProps} initialContent="<p>Initial content</p>" />
|
|
4073
|
+
</TestWrapper>
|
|
4074
|
+
);
|
|
4075
|
+
|
|
4076
|
+
act(() => {
|
|
4077
|
+
jest.runAllTimers();
|
|
4078
|
+
});
|
|
4079
|
+
|
|
4080
|
+
expect(updateContent).toHaveBeenCalledWith('<p>Initial content</p>', true);
|
|
4081
|
+
});
|
|
4082
|
+
|
|
4083
|
+
it('inapp variant runs updateContent when device content differs from initialContent string', () => {
|
|
4084
|
+
const updateContent = jest.fn();
|
|
4085
|
+
require('../hooks/useInAppContent').useInAppContent = () => ({
|
|
4086
|
+
activeDevice: 'android',
|
|
4087
|
+
keepContentSame: false,
|
|
4088
|
+
updateContent,
|
|
4089
|
+
saveContent: jest.fn(),
|
|
4090
|
+
markAsSaved: jest.fn(),
|
|
4091
|
+
switchDevice: jest.fn(),
|
|
4092
|
+
toggleContentSync: jest.fn(),
|
|
4093
|
+
getDeviceContent: () => '<p>android content</p>',
|
|
4094
|
+
setDeviceContent: jest.fn(),
|
|
4095
|
+
getContentSize: () => 20,
|
|
4096
|
+
isLoading: false,
|
|
4097
|
+
isDirty: false,
|
|
4098
|
+
hasContent: true,
|
|
4099
|
+
});
|
|
4100
|
+
|
|
4101
|
+
render(
|
|
4102
|
+
<TestWrapper>
|
|
4103
|
+
<HTMLEditor
|
|
4104
|
+
{...defaultProps}
|
|
4105
|
+
variant="inapp"
|
|
4106
|
+
initialContent="<p>Initial content</p>"
|
|
4107
|
+
/>
|
|
4108
|
+
</TestWrapper>
|
|
4109
|
+
);
|
|
4110
|
+
|
|
4111
|
+
act(() => {
|
|
4112
|
+
jest.runAllTimers();
|
|
4113
|
+
});
|
|
4114
|
+
|
|
4115
|
+
expect(updateContent).toHaveBeenCalledWith('<p>Initial content</p>', true);
|
|
4116
|
+
});
|
|
4117
|
+
|
|
4118
|
+
it('handleLabelInsert uses numeric cursor when getCursor is not a function', () => {
|
|
4119
|
+
const CapNotification = require('@capillarytech/cap-ui-library/CapNotification');
|
|
4120
|
+
mockCodeEditorOptions.omitGetCursor = true;
|
|
4121
|
+
|
|
4122
|
+
render(
|
|
4123
|
+
<TestWrapper>
|
|
4124
|
+
<HTMLEditor {...defaultProps} />
|
|
4125
|
+
</TestWrapper>
|
|
4126
|
+
);
|
|
4127
|
+
|
|
4128
|
+
act(() => {
|
|
4129
|
+
jest.runAllTimers();
|
|
4130
|
+
});
|
|
4131
|
+
|
|
4132
|
+
fireEvent.click(screen.getByText('Insert Label (Null Position)'));
|
|
4133
|
+
|
|
4134
|
+
expect(CapNotification.success).toHaveBeenCalled();
|
|
4135
|
+
});
|
|
4136
|
+
|
|
4137
|
+
it('handleValidationErrorClick with undefined error focuses editor when ref is set', () => {
|
|
4138
|
+
mockUseValidationImpl.mockReturnValue({
|
|
4139
|
+
isValidating: false,
|
|
4140
|
+
getAllIssues: () => [{ source: 'htmlhint', rule: 'rule1', message: 'E' }],
|
|
4141
|
+
isClean: () => false,
|
|
4142
|
+
summary: { totalErrors: 1, totalWarnings: 0 },
|
|
4143
|
+
});
|
|
4144
|
+
|
|
4145
|
+
render(
|
|
4146
|
+
<TestWrapper>
|
|
4147
|
+
<HTMLEditor {...defaultProps} />
|
|
4148
|
+
</TestWrapper>
|
|
4149
|
+
);
|
|
4150
|
+
|
|
4151
|
+
act(() => {
|
|
4152
|
+
jest.runAllTimers();
|
|
4153
|
+
});
|
|
4154
|
+
|
|
4155
|
+
fireEvent.click(screen.getByText('Error undefined'));
|
|
4156
|
+
});
|
|
4157
|
+
|
|
4158
|
+
it('handleValidationErrorClick skips editor when ref is not available', () => {
|
|
4159
|
+
mockCodeEditorOptions.setRef = false;
|
|
4160
|
+
mockUseValidationImpl.mockReturnValue({
|
|
4161
|
+
isValidating: false,
|
|
4162
|
+
getAllIssues: () => [{ source: 'htmlhint', rule: 'rule1', message: 'E' }],
|
|
4163
|
+
isClean: () => false,
|
|
4164
|
+
summary: { totalErrors: 1, totalWarnings: 0 },
|
|
4165
|
+
});
|
|
4166
|
+
|
|
4167
|
+
render(
|
|
4168
|
+
<TestWrapper>
|
|
4169
|
+
<HTMLEditor {...defaultProps} onErrorAcknowledged={jest.fn()} />
|
|
4170
|
+
</TestWrapper>
|
|
4171
|
+
);
|
|
4172
|
+
|
|
4173
|
+
act(() => {
|
|
4174
|
+
jest.runAllTimers();
|
|
4175
|
+
});
|
|
4176
|
+
|
|
4177
|
+
fireEvent.click(screen.getByText('Error at Line 5'));
|
|
4178
|
+
});
|
|
4179
|
+
|
|
4180
|
+
it('getIssueCounts treats nullish issues as non-blocking (isBlockingError issue || {})', async () => {
|
|
4181
|
+
const ref = React.createRef();
|
|
4182
|
+
const WrappedHTMLEditor = HTMLEditor.WrappedComponent || HTMLEditor;
|
|
4183
|
+
const intlProvider = new IntlProvider({ locale: 'en', messages: {} }, {});
|
|
4184
|
+
const { intl } = intlProvider.getChildContext();
|
|
4185
|
+
|
|
4186
|
+
mockUseValidationImpl.mockReturnValue({
|
|
4187
|
+
isValidating: false,
|
|
4188
|
+
hasBlockingErrors: false,
|
|
4189
|
+
getAllIssues: () => [undefined, null, {}],
|
|
4190
|
+
isClean: () => false,
|
|
4191
|
+
summary: { totalErrors: 0, totalWarnings: 3 },
|
|
4192
|
+
});
|
|
4193
|
+
|
|
4194
|
+
render(
|
|
4195
|
+
<TestWrapper>
|
|
4196
|
+
<WrappedHTMLEditor {...defaultProps} intl={intl} ref={ref} />
|
|
4197
|
+
</TestWrapper>
|
|
4198
|
+
);
|
|
4199
|
+
|
|
4200
|
+
act(() => {
|
|
4201
|
+
jest.runAllTimers();
|
|
4202
|
+
});
|
|
4203
|
+
|
|
4204
|
+
await waitFor(() => {
|
|
4205
|
+
expect(ref.current).toBeTruthy();
|
|
4206
|
+
});
|
|
4207
|
+
|
|
4208
|
+
expect(ref.current.getIssueCounts()).toEqual({
|
|
4209
|
+
errors: 0,
|
|
4210
|
+
warnings: 3,
|
|
4211
|
+
total: 3,
|
|
4212
|
+
});
|
|
4213
|
+
});
|
|
4214
|
+
|
|
4215
|
+
it('handleContextChange uses embedded full when location.query is missing', () => {
|
|
4216
|
+
const globalActions = { fetchSchemaForEntity: jest.fn() };
|
|
4217
|
+
const location = { pathname: '/creatives' };
|
|
4218
|
+
|
|
4219
|
+
render(
|
|
4220
|
+
<TestWrapper>
|
|
4221
|
+
<HTMLEditor
|
|
4222
|
+
{...defaultProps}
|
|
4223
|
+
globalActions={globalActions}
|
|
4224
|
+
location={location}
|
|
4225
|
+
variant="email"
|
|
4226
|
+
/>
|
|
4227
|
+
</TestWrapper>
|
|
4228
|
+
);
|
|
4229
|
+
|
|
4230
|
+
act(() => {
|
|
4231
|
+
jest.runAllTimers();
|
|
4232
|
+
});
|
|
4233
|
+
|
|
4234
|
+
fireEvent.click(screen.getByTestId('trigger-context-change'));
|
|
4235
|
+
|
|
4236
|
+
expect(globalActions.fetchSchemaForEntity).toHaveBeenCalledWith({
|
|
4237
|
+
layout: 'EMAIL',
|
|
4238
|
+
type: 'TAG',
|
|
4239
|
+
context: 'test-context',
|
|
4240
|
+
embedded: 'full',
|
|
4241
|
+
});
|
|
4242
|
+
});
|
|
4243
|
+
|
|
4244
|
+
it('handleSave succeeds when content.content is undefined', () => {
|
|
4245
|
+
const CapNotification = require('@capillarytech/cap-ui-library/CapNotification');
|
|
4246
|
+
const onSave = jest.fn();
|
|
4247
|
+
|
|
4248
|
+
require('../hooks/useEditorContent').useEditorContent = () => ({
|
|
4249
|
+
content: undefined,
|
|
4250
|
+
updateContent: jest.fn(),
|
|
4251
|
+
saveContent: jest.fn(),
|
|
4252
|
+
markAsSaved: jest.fn(),
|
|
4253
|
+
isLoading: false,
|
|
4254
|
+
isDirty: false,
|
|
4255
|
+
hasContent: false,
|
|
4256
|
+
getContentSize: jest.fn(() => 0),
|
|
4257
|
+
});
|
|
4258
|
+
|
|
4259
|
+
render(
|
|
4260
|
+
<TestWrapper>
|
|
4261
|
+
<HTMLEditor {...defaultProps} onSave={onSave} />
|
|
4262
|
+
</TestWrapper>
|
|
4263
|
+
);
|
|
4264
|
+
|
|
4265
|
+
act(() => {
|
|
4266
|
+
jest.runAllTimers();
|
|
4267
|
+
});
|
|
4268
|
+
|
|
4269
|
+
fireEvent.click(screen.getByText('Save'));
|
|
4270
|
+
|
|
4271
|
+
expect(onSave).toHaveBeenCalledWith({ html: undefined, css: undefined, javascript: undefined });
|
|
4272
|
+
expect(CapNotification.success).toHaveBeenCalled();
|
|
4273
|
+
});
|
|
4274
|
+
|
|
4275
|
+
it('countIssuesBySeverity handles null allIssues from getAllIssues', async () => {
|
|
4276
|
+
const ref = React.createRef();
|
|
4277
|
+
const WrappedHTMLEditor = HTMLEditor.WrappedComponent || HTMLEditor;
|
|
4278
|
+
const intlProvider = new IntlProvider({ locale: 'en', messages: {} }, {});
|
|
4279
|
+
const { intl } = intlProvider.getChildContext();
|
|
4280
|
+
|
|
4281
|
+
mockUseValidationImpl.mockReturnValue({
|
|
4282
|
+
isValidating: false,
|
|
4283
|
+
getAllIssues: () => null,
|
|
4284
|
+
isClean: () => true,
|
|
4285
|
+
summary: { totalErrors: 0, totalWarnings: 0 },
|
|
4286
|
+
});
|
|
4287
|
+
|
|
4288
|
+
render(
|
|
4289
|
+
<TestWrapper>
|
|
4290
|
+
<WrappedHTMLEditor {...defaultProps} intl={intl} ref={ref} />
|
|
4291
|
+
</TestWrapper>
|
|
4292
|
+
);
|
|
4293
|
+
|
|
4294
|
+
act(() => {
|
|
4295
|
+
jest.runAllTimers();
|
|
4296
|
+
});
|
|
4297
|
+
|
|
4298
|
+
await waitFor(() => {
|
|
4299
|
+
expect(ref.current).toBeTruthy();
|
|
4300
|
+
});
|
|
4301
|
+
|
|
4302
|
+
expect(ref.current.getIssueCounts()).toEqual({
|
|
4303
|
+
errors: 0,
|
|
4304
|
+
warnings: 0,
|
|
4305
|
+
total: 0,
|
|
4306
|
+
});
|
|
4307
|
+
});
|
|
4308
|
+
|
|
4309
|
+
it('handleContextChange lowercases null contextData to empty string', () => {
|
|
4310
|
+
const globalActions = { fetchSchemaForEntity: jest.fn() };
|
|
4311
|
+
const location = { query: { type: 'full' } };
|
|
4312
|
+
|
|
4313
|
+
render(
|
|
4314
|
+
<TestWrapper>
|
|
4315
|
+
<HTMLEditor
|
|
4316
|
+
{...defaultProps}
|
|
4317
|
+
globalActions={globalActions}
|
|
4318
|
+
location={location}
|
|
4319
|
+
variant="email"
|
|
4320
|
+
/>
|
|
4321
|
+
</TestWrapper>
|
|
4322
|
+
);
|
|
4323
|
+
|
|
4324
|
+
act(() => {
|
|
4325
|
+
jest.runAllTimers();
|
|
4326
|
+
});
|
|
4327
|
+
|
|
4328
|
+
fireEvent.click(screen.getByTestId('trigger-context-null'));
|
|
4329
|
+
|
|
4330
|
+
expect(globalActions.fetchSchemaForEntity).toHaveBeenCalledWith({
|
|
4331
|
+
layout: 'EMAIL',
|
|
4332
|
+
type: 'TAG',
|
|
4333
|
+
context: '',
|
|
4334
|
+
embedded: 'full',
|
|
4335
|
+
});
|
|
4336
|
+
});
|
|
4337
|
+
|
|
4338
|
+
it('inapp initialContent object falls back to ANDROID when active device slot is missing', () => {
|
|
4339
|
+
const updateContent = jest.fn();
|
|
4340
|
+
require('../hooks/useInAppContent').useInAppContent = () => ({
|
|
4341
|
+
activeDevice: 'android',
|
|
4342
|
+
keepContentSame: false,
|
|
4343
|
+
updateContent,
|
|
4344
|
+
saveContent: jest.fn(),
|
|
4345
|
+
markAsSaved: jest.fn(),
|
|
4346
|
+
switchDevice: jest.fn(),
|
|
4347
|
+
toggleContentSync: jest.fn(),
|
|
4348
|
+
getDeviceContent: () => '<p>old</p>',
|
|
4349
|
+
setDeviceContent: jest.fn(),
|
|
4350
|
+
getContentSize: () => 20,
|
|
4351
|
+
isLoading: false,
|
|
4352
|
+
isDirty: false,
|
|
4353
|
+
hasContent: true,
|
|
4354
|
+
});
|
|
4355
|
+
|
|
4356
|
+
render(
|
|
4357
|
+
<TestWrapper>
|
|
4358
|
+
<HTMLEditor
|
|
4359
|
+
{...defaultProps}
|
|
4360
|
+
variant="inapp"
|
|
4361
|
+
initialContent={{ ios: '<p>ios only</p>' }}
|
|
4362
|
+
/>
|
|
4363
|
+
</TestWrapper>
|
|
4364
|
+
);
|
|
4365
|
+
|
|
4366
|
+
act(() => {
|
|
4367
|
+
jest.runAllTimers();
|
|
4368
|
+
});
|
|
4369
|
+
|
|
4370
|
+
expect(updateContent).toHaveBeenCalledWith('', true);
|
|
4371
|
+
});
|
|
4372
|
+
|
|
4373
|
+
it('inapp initialContent effect skips when updateContent is not available', () => {
|
|
4374
|
+
require('../hooks/useInAppContent').useInAppContent = () => ({
|
|
4375
|
+
activeDevice: 'android',
|
|
4376
|
+
keepContentSame: false,
|
|
4377
|
+
updateContent: undefined,
|
|
4378
|
+
saveContent: jest.fn(),
|
|
4379
|
+
markAsSaved: jest.fn(),
|
|
4380
|
+
switchDevice: jest.fn(),
|
|
4381
|
+
toggleContentSync: jest.fn(),
|
|
4382
|
+
getDeviceContent: () => '<p>x</p>',
|
|
4383
|
+
setDeviceContent: jest.fn(),
|
|
4384
|
+
getContentSize: () => 20,
|
|
4385
|
+
isLoading: false,
|
|
4386
|
+
isDirty: false,
|
|
4387
|
+
hasContent: true,
|
|
4388
|
+
});
|
|
4389
|
+
|
|
4390
|
+
render(
|
|
4391
|
+
<TestWrapper>
|
|
4392
|
+
<HTMLEditor
|
|
4393
|
+
{...defaultProps}
|
|
4394
|
+
variant="inapp"
|
|
4395
|
+
initialContent="<p>y</p>"
|
|
4396
|
+
/>
|
|
4397
|
+
</TestWrapper>
|
|
4398
|
+
);
|
|
4399
|
+
|
|
4400
|
+
act(() => {
|
|
4401
|
+
jest.runAllTimers();
|
|
4402
|
+
});
|
|
4403
|
+
|
|
4404
|
+
expect(screen.getByTestId('device-toggle')).toBeInTheDocument();
|
|
4405
|
+
});
|
|
4406
|
+
|
|
4407
|
+
it('uses default forwardRef props when only intl is supplied', () => {
|
|
4408
|
+
const WrappedHTMLEditor = HTMLEditor.WrappedComponent || HTMLEditor;
|
|
4409
|
+
const intlProvider = new IntlProvider({ locale: 'en', messages: {} }, {});
|
|
4410
|
+
const { intl } = intlProvider.getChildContext();
|
|
4411
|
+
|
|
4412
|
+
render(
|
|
4413
|
+
<TestWrapper>
|
|
4414
|
+
<WrappedHTMLEditor intl={intl} />
|
|
4415
|
+
</TestWrapper>
|
|
4416
|
+
);
|
|
4417
|
+
|
|
4418
|
+
act(() => {
|
|
4419
|
+
jest.runAllTimers();
|
|
4420
|
+
});
|
|
4421
|
+
|
|
4422
|
+
expect(screen.getByTestId('editor-toolbar')).toBeInTheDocument();
|
|
4423
|
+
});
|
|
4424
|
+
|
|
4425
|
+
it('main validation effect notifies when isContentEmpty changes (hasChanged branch)', () => {
|
|
4426
|
+
const onValidationChange = jest.fn();
|
|
4427
|
+
let hookContent = '';
|
|
4428
|
+
|
|
4429
|
+
require('../hooks/useEditorContent').useEditorContent = () => ({
|
|
4430
|
+
content: hookContent,
|
|
4431
|
+
updateContent: jest.fn(),
|
|
4432
|
+
saveContent: jest.fn(),
|
|
4433
|
+
markAsSaved: jest.fn(),
|
|
4434
|
+
isLoading: false,
|
|
4435
|
+
isDirty: false,
|
|
4436
|
+
hasContent: !!hookContent,
|
|
4437
|
+
getContentSize: jest.fn(() => hookContent.length),
|
|
4438
|
+
});
|
|
4439
|
+
|
|
4440
|
+
const { rerender } = render(
|
|
4441
|
+
<TestWrapper>
|
|
4442
|
+
<HTMLEditor {...defaultProps} onValidationChange={onValidationChange} />
|
|
4443
|
+
</TestWrapper>
|
|
4444
|
+
);
|
|
4445
|
+
|
|
4446
|
+
act(() => {
|
|
4447
|
+
jest.runAllTimers();
|
|
4448
|
+
});
|
|
4449
|
+
|
|
4450
|
+
onValidationChange.mockClear();
|
|
4451
|
+
|
|
4452
|
+
hookContent = '<p>not empty</p>';
|
|
4453
|
+
|
|
4454
|
+
rerender(
|
|
4455
|
+
<TestWrapper>
|
|
4456
|
+
<HTMLEditor {...defaultProps} onValidationChange={onValidationChange} />
|
|
4457
|
+
</TestWrapper>
|
|
4458
|
+
);
|
|
4459
|
+
|
|
4460
|
+
act(() => {
|
|
4461
|
+
jest.runAllTimers();
|
|
4462
|
+
});
|
|
4463
|
+
|
|
4464
|
+
expect(onValidationChange).toHaveBeenCalled();
|
|
4465
|
+
const lastCall = onValidationChange.mock.calls[onValidationChange.mock.calls.length - 1][0];
|
|
4466
|
+
expect(lastCall.isContentEmpty).toBe(false);
|
|
4467
|
+
});
|
|
4468
|
+
});
|
|
3544
4469
|
});
|