@capillarytech/creatives-library 8.0.325 → 8.0.326

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/package.json +1 -1
  2. package/utils/tests/tagValidations.test.js +0 -34
  3. package/v2Components/CapTagList/index.js +22 -14
  4. package/v2Components/CapTagList/style.scss +0 -48
  5. package/v2Components/CapTagListWithInput/index.js +0 -4
  6. package/v2Components/CapWhatsappCTA/index.js +0 -2
  7. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +1 -2
  8. package/v2Components/FormBuilder/index.js +0 -7
  9. package/v2Components/HtmlEditor/HTMLEditor.js +1 -6
  10. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +0 -1
  11. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +2 -927
  12. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +0 -3
  13. package/v2Containers/BeeEditor/index.js +0 -3
  14. package/v2Containers/CreativesContainer/SlideBoxContent.js +1 -28
  15. package/v2Containers/CreativesContainer/index.js +6 -10
  16. package/v2Containers/Email/index.js +0 -1
  17. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +1 -7
  18. package/v2Containers/EmailWrapper/components/EmailWrapperView.js +0 -3
  19. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +2 -20
  20. package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +1 -16
  21. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +0 -3
  22. package/v2Containers/EmailWrapper/index.js +0 -4
  23. package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +0 -1
  24. package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +0 -9
  25. package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +0 -19
  26. package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +0 -3
  27. package/v2Containers/InAppWrapper/index.js +0 -3
  28. package/v2Containers/MobilePush/Create/index.js +0 -2
  29. package/v2Containers/MobilePush/Edit/index.js +0 -2
  30. package/v2Containers/MobilepushWrapper/index.js +1 -3
  31. package/v2Containers/Rcs/index.js +1 -9
  32. package/v2Containers/Sms/Create/index.js +0 -2
  33. package/v2Containers/Sms/Edit/index.js +0 -2
  34. package/v2Containers/SmsTrai/Edit/index.js +0 -2
  35. package/v2Containers/SmsWrapper/index.js +0 -2
  36. package/v2Containers/TagList/index.js +2 -41
  37. package/v2Containers/TagList/messages.js +0 -4
  38. package/v2Containers/TagList/tests/TagList.test.js +20 -122
  39. package/v2Containers/TagList/tests/mockdata.js +0 -17
  40. package/v2Containers/Viber/index.js +0 -5
  41. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +2 -0
  42. package/v2Containers/WebPush/Create/index.js +1 -9
  43. package/v2Containers/Whatsapp/index.js +0 -5
  44. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +0 -20
  45. package/v2Containers/Zalo/index.js +0 -2
  46. package/v2Components/CapTagListWithInput/__tests__/CapTagListWithInput.test.js +0 -63
@@ -11,8 +11,6 @@ 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';
16
14
 
17
15
  // Options to control CodeEditorPane mock behavior
18
16
  const mockCodeEditorOptions = {
@@ -21,7 +19,6 @@ const mockCodeEditorOptions = {
21
19
  setRef: true,
22
20
  navigateToLineThrows: false,
23
21
  includeNavigateToLine: true,
24
- omitGetCursor: false,
25
22
  };
26
23
 
27
24
 
@@ -169,14 +166,11 @@ jest.mock('../components/CodeEditorPane', () => {
169
166
 
170
167
  const methods = {
171
168
  focus: jest.fn(),
169
+ getCursor: jest.fn(() => 0),
172
170
  getValue: jest.fn(() => value),
173
171
  setValue: jest.fn((newValue) => setValue(newValue)),
174
172
  };
175
173
 
176
- if (!mockCodeEditorOptions.omitGetCursor) {
177
- methods.getCursor = jest.fn(() => 0);
178
- }
179
-
180
174
  if (mockCodeEditorOptions.includeNavigateToLine) {
181
175
  methods.navigateToLine = jest.fn(() => {
182
176
  if (mockCodeEditorOptions.navigateToLineThrows) {
@@ -205,20 +199,7 @@ jest.mock('../components/CodeEditorPane', () => {
205
199
  }
206
200
 
207
201
  return (
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
- >
202
+ <div data-testid="code-editor-pane">
222
203
  <textarea
223
204
  value={value}
224
205
  onChange={(e) => {
@@ -235,18 +216,6 @@ jest.mock('../components/CodeEditorPane', () => {
235
216
  >
236
217
  Trigger Context Change
237
218
  </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>
250
219
  {validation && (
251
220
  <ValidationErrorDisplay
252
221
  validation={validation}
@@ -280,9 +249,6 @@ jest.mock('../components/ValidationErrorDisplay', () => function MockValidationE
280
249
  <button onClick={() => onErrorClick && onErrorClick({ line: null })}>
281
250
  Error without Line
282
251
  </button>
283
- <button onClick={() => onErrorClick && onErrorClick(undefined)}>
284
- Error undefined
285
- </button>
286
252
  </div>
287
253
  );
288
254
  });
@@ -418,195 +384,6 @@ describe('HTMLEditor', () => {
418
384
  });
419
385
  });
420
386
 
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
-
610
387
  describe('Context Provider', () => {
611
388
  it('provides correct context value for email variant', () => {
612
389
  render(
@@ -2702,7 +2479,6 @@ describe('HTMLEditor', () => {
2702
2479
  mockCodeEditorOptions.setRef = true;
2703
2480
  mockCodeEditorOptions.navigateToLineThrows = false;
2704
2481
  mockCodeEditorOptions.includeNavigateToLine = true;
2705
- mockCodeEditorOptions.omitGetCursor = false;
2706
2482
 
2707
2483
  // Clear specific mocks instead of all to avoid breaking other mocks
2708
2484
  CapNotification.warning.mockClear();
@@ -3691,7 +3467,6 @@ describe('HTMLEditor', () => {
3691
3467
  tags={null}
3692
3468
  injectedTags={null}
3693
3469
  eventContextTags={null}
3694
- waitEventContextTags={null}
3695
3470
  selectedOfferDetails={null}
3696
3471
  onTagContextChange={null}
3697
3472
  onTagSelect={null}
@@ -3717,7 +3492,6 @@ describe('HTMLEditor', () => {
3717
3492
  <HTMLEditor
3718
3493
  tags={[]}
3719
3494
  eventContextTags={[]}
3720
- waitEventContextTags={{}}
3721
3495
  selectedOfferDetails={[]}
3722
3496
  />
3723
3497
  </TestWrapper>
@@ -3767,703 +3541,4 @@ describe('HTMLEditor', () => {
3767
3541
  expect(screen.getByTestId('editor-toolbar')).toBeInTheDocument();
3768
3542
  });
3769
3543
  });
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
- });
4469
3544
  });