@capillarytech/creatives-library 9.0.26 → 9.0.29-alpha.1

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 (149) hide show
  1. package/constants/unified.js +29 -0
  2. package/global-styles.js +1 -1
  3. package/package.json +1 -1
  4. package/services/tests/api.test.js +35 -20
  5. package/utils/commonUtils.js +22 -1
  6. package/utils/rcsPayloadUtils.js +102 -0
  7. package/utils/templateVarUtils.js +198 -0
  8. package/utils/tests/rcsPayloadUtils.test.js +295 -0
  9. package/utils/tests/templateVarUtils.test.js +204 -0
  10. package/v2Components/CapActionButton/constants.js +21 -1
  11. package/v2Components/CapActionButton/index.js +260 -144
  12. package/v2Components/CapActionButton/index.scss +245 -14
  13. package/v2Components/CapActionButton/messages.js +32 -3
  14. package/v2Components/CapActionButton/tests/index.test.js +74 -19
  15. package/v2Components/CapImageUpload/index.js +2 -2
  16. package/v2Components/CapTagList/index.js +9 -0
  17. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -48
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  23. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +58 -11
  24. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +186 -22
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +381 -80
  27. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +155 -8
  28. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  29. package/v2Components/CommonTestAndPreview/constants.js +35 -2
  30. package/v2Components/CommonTestAndPreview/index.js +770 -231
  31. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  32. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  33. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  34. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +1 -0
  35. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +412 -257
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +337 -63
  37. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  38. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  39. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  40. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +108 -15
  41. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  42. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  43. package/v2Components/CommonTestAndPreview/tests/index.test.js +133 -4
  44. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  45. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
  46. package/v2Components/CommonTestAndPreview/tests/utils.test.js +151 -0
  47. package/v2Components/CommonTestAndPreview/utils.js +84 -0
  48. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  49. package/v2Components/SmsFallback/constants.js +94 -0
  50. package/v2Components/SmsFallback/index.js +958 -0
  51. package/v2Components/SmsFallback/index.scss +266 -0
  52. package/v2Components/SmsFallback/messages.js +78 -0
  53. package/v2Components/SmsFallback/smsFallbackUtils.js +120 -0
  54. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  55. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  56. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  57. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +208 -0
  58. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  59. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +471 -0
  60. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  61. package/v2Components/TemplatePreview/_templatePreview.scss +45 -25
  62. package/v2Components/TemplatePreview/constants.js +2 -0
  63. package/v2Components/TemplatePreview/index.js +147 -32
  64. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  65. package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +6 -6
  66. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  67. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  68. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  69. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  70. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  71. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  72. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +4 -1
  73. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +4 -0
  74. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +20 -20
  75. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +27 -0
  76. package/v2Containers/CreativesContainer/SlideBoxContent.js +37 -4
  77. package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
  78. package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
  79. package/v2Containers/CreativesContainer/constants.js +11 -0
  80. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +77 -0
  81. package/v2Containers/CreativesContainer/index.js +328 -106
  82. package/v2Containers/CreativesContainer/index.scss +102 -1
  83. package/v2Containers/CreativesContainer/tests/CreativesSlideBoxWrapper.test.js +58 -0
  84. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  85. package/v2Containers/CreativesContainer/tests/SlideBoxContent.smsDltPreview.test.js +73 -0
  86. package/v2Containers/CreativesContainer/tests/SlideBoxContent.test.js +37 -0
  87. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +103 -34
  88. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +193 -15
  89. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +88 -0
  90. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +549 -57
  91. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  92. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  93. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  94. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  95. package/v2Containers/Email/index.js +3 -54
  96. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +4 -9
  97. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  98. package/v2Containers/Rcs/carouselUtils.js +224 -0
  99. package/v2Containers/Rcs/components/CarouselCard.js +317 -0
  100. package/v2Containers/Rcs/components/CarouselCardButtons.js +113 -0
  101. package/v2Containers/Rcs/components/CarouselCardMedia.js +136 -0
  102. package/v2Containers/Rcs/components/CarouselCharacterCount.js +31 -0
  103. package/v2Containers/Rcs/components/CarouselDimensionSelection.js +80 -0
  104. package/v2Containers/Rcs/constants.js +132 -16
  105. package/v2Containers/Rcs/index.js +1808 -949
  106. package/v2Containers/Rcs/index.scss +443 -8
  107. package/v2Containers/Rcs/messages.js +45 -22
  108. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +484 -0
  109. package/v2Containers/Rcs/tests/CarouselCard.test.js +464 -0
  110. package/v2Containers/Rcs/tests/CarouselCardButtons.test.js +211 -0
  111. package/v2Containers/Rcs/tests/CarouselCardMedia.test.js +160 -0
  112. package/v2Containers/Rcs/tests/CarouselCharacterCount.test.js +50 -0
  113. package/v2Containers/Rcs/tests/CarouselDimensionSelection.test.js +119 -0
  114. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +74176 -39409
  115. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +10 -74
  116. package/v2Containers/Rcs/tests/carouselUtils.test.js +916 -0
  117. package/v2Containers/Rcs/tests/index.test.js +219 -40
  118. package/v2Containers/Rcs/tests/mockData.js +38 -0
  119. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +873 -0
  120. package/v2Containers/Rcs/tests/utils.test.js +682 -30
  121. package/v2Containers/Rcs/utils.js +514 -12
  122. package/v2Containers/Sms/Create/index.js +115 -48
  123. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  124. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  125. package/v2Containers/SmsTrai/Create/index.js +9 -4
  126. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  127. package/v2Containers/SmsTrai/Edit/index.js +678 -169
  128. package/v2Containers/SmsTrai/Edit/index.scss +126 -0
  129. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  130. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
  131. package/v2Containers/SmsWrapper/index.js +37 -8
  132. package/v2Containers/TagList/index.js +5 -0
  133. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  134. package/v2Containers/Templates/_templates.scss +196 -12
  135. package/v2Containers/Templates/actions.js +11 -0
  136. package/v2Containers/Templates/constants.js +2 -0
  137. package/v2Containers/Templates/index.js +131 -59
  138. package/v2Containers/Templates/sagas.js +57 -13
  139. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +129 -0
  140. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
  141. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  142. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  143. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  144. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  145. package/v2Containers/TemplatesV2/index.js +88 -25
  146. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  147. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  148. package/v2Containers/Whatsapp/index.js +3 -20
  149. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -1,83 +1,108 @@
1
1
  /**
2
- * Tests for CustomValuesEditor component.
3
- * Covers: loading state, values-missing warning, JSON view, tag table, discard/update buttons.
4
2
  * @jest-environment jsdom
5
3
  */
6
-
7
4
  import React from 'react';
8
5
  import { render, screen, fireEvent } from '@testing-library/react';
6
+ import '@testing-library/jest-dom';
9
7
  import { IntlProvider } from 'react-intl';
10
8
  import PropTypes from 'prop-types';
9
+ import CustomValuesEditor from '../CustomValuesEditor';
11
10
 
12
- jest.mock('@capillarytech/cap-ui-library/CapRow', () => ({ children, className }) => (
13
- <div className={className}>{children}</div>
14
- ));
15
- jest.mock('@capillarytech/cap-ui-library/CapSpin', () => ({ size }) => (
16
- <div data-testid="cap-spin" data-size={size}>Loading...</div>
17
- ));
18
- jest.mock('@capillarytech/cap-ui-library/CapSwitch', () => ({ checked, onChange }) => (
19
- <button
20
- type="button"
21
- data-testid="cap-switch"
22
- aria-checked={String(checked)}
23
- onClick={() => onChange(!checked)}
24
- >
25
- Toggle
26
- </button>
27
- ));
28
- jest.mock('@capillarytech/cap-ui-library/CapButton', () => ({ children, onClick, disabled, loading, type: btnType, icon }) => (
29
- <button
30
- type="button"
31
- data-testid="cap-button"
32
- data-btntype={btnType}
33
- data-icon={icon}
34
- onClick={onClick}
35
- disabled={!!(disabled || loading)}
36
- >
37
- {children}
38
- </button>
39
- ));
40
- jest.mock('@capillarytech/cap-ui-library/CapInput', () => ({ value, onChange, placeholder, isRequired, className }) => (
41
- <input
42
- data-testid="cap-input"
43
- className={className}
44
- value={value || ''}
45
- onChange={onChange}
46
- placeholder={placeholder}
47
- required={!!isRequired}
48
- />
49
- ));
50
- jest.mock('@capillarytech/cap-ui-library/CapLabel', () => ({ children, type, className }) => (
51
- <span data-testid="cap-label" data-type={type} className={className}>{children}</span>
52
- ));
11
+ jest.mock('@capillarytech/cap-ui-library/CapRow', () => {
12
+ const React = require('react');
13
+ return function CapRow(props) {
14
+ return React.createElement('div', { className: props?.className, 'data-testid': 'cap-row' }, props?.children);
15
+ };
16
+ });
17
+ jest.mock('@capillarytech/cap-ui-library/CapSpin', () => {
18
+ const React = require('react');
19
+ return function CapSpin() {
20
+ return React.createElement('div', { 'data-testid': 'cap-spin' }, 'spin');
21
+ };
22
+ });
23
+ jest.mock('@capillarytech/cap-ui-library/CapSwitch', () => {
24
+ const React = require('react');
25
+ return function CapSwitch(props) {
26
+ return React.createElement('input', {
27
+ type: 'checkbox',
28
+ 'data-testid': 'json-switch',
29
+ checked: props?.checked,
30
+ onChange: (e) => props?.onChange?.(e.target.checked),
31
+ });
32
+ };
33
+ });
34
+ jest.mock('@capillarytech/cap-ui-library/CapButton', () => {
35
+ const React = require('react');
36
+ return function CapButton(props) {
37
+ return React.createElement('button', {
38
+ type: 'button',
39
+ 'data-testid': 'cap-button',
40
+ onClick: props?.onClick,
41
+ disabled: props?.disabled,
42
+ 'data-loading': props?.loading,
43
+ }, props?.children);
44
+ };
45
+ });
46
+ jest.mock('@capillarytech/cap-ui-library/CapInput', () => {
47
+ const React = require('react');
48
+ return function CapInput(props) {
49
+ return React.createElement('input', {
50
+ 'data-testid': 'tag-input',
51
+ value: props?.value,
52
+ placeholder: props?.placeholder,
53
+ onChange: props?.onChange,
54
+ });
55
+ };
56
+ });
57
+ jest.mock('@capillarytech/cap-ui-library/CapLabel', () => {
58
+ const React = require('react');
59
+ return function CapLabel(props) {
60
+ return React.createElement('div', { className: props?.className, 'data-testid': 'cap-label' }, props?.children);
61
+ };
62
+ });
53
63
 
54
- import CustomValuesEditor from '../CustomValuesEditor';
64
+ const formatMessage = (msg) => (typeof msg === 'object' && msg?.id ? msg.id : String(msg));
55
65
 
56
- const defaultProps = {
66
+ const baseProps = {
57
67
  isExtractingTags: false,
58
68
  isUpdatePreviewDisabled: false,
59
69
  showJSON: false,
60
70
  setShowJSON: jest.fn(),
61
- customValues: { '{{customer.name}}': 'Alice', '{{customer.email}}': '' },
71
+ customValues: { 'tag.a': 'v1' },
62
72
  handleJSONTextChange: jest.fn(),
63
- extractedTags: [
64
- { fullPath: '{{customer.name}}' },
65
- { fullPath: '{{customer.email}}' },
73
+ sections: [
74
+ {
75
+ key: 'sec1',
76
+ title: 'Section A',
77
+ requiredTags: [{ fullPath: 'tag.a', name: 'a' }],
78
+ optionalTags: [{ fullPath: 'tag.b', name: 'b' }],
79
+ },
66
80
  ],
67
- requiredTags: [{ fullPath: '{{customer.name}}' }],
68
- optionalTags: [{ fullPath: '{{customer.email}}' }],
69
81
  handleCustomValueChange: jest.fn(),
70
82
  handleDiscardCustomValues: jest.fn(),
71
83
  handleUpdatePreview: jest.fn(),
72
84
  isUpdatingPreview: false,
73
- formatMessage: jest.fn((msg) => msg.defaultMessage || msg.id),
85
+ formatMessage,
86
+ };
87
+
88
+ const defaultProps = {
89
+ ...baseProps,
90
+ customValues: {},
91
+ sections: [
92
+ {
93
+ key: 'default-sec',
94
+ title: 'Section',
95
+ requiredTags: [{ fullPath: '{{customer.name}}', name: 'customer.name' }],
96
+ optionalTags: [{ fullPath: '{{customer.email}}', name: 'customer.email' }],
97
+ },
98
+ ],
74
99
  };
75
100
 
76
101
  // IntlProvider wrapper for incoming tests
77
102
  const scope = 'app.v2Components.TestAndPreviewSlidebox';
78
103
  const mockMessages = {
79
104
  [`${scope}.extractingTags`]: 'Extracting tags...',
80
- [`${scope}.valuesMissing`]: 'Values missing for some of the personalization tags',
105
+ [`${scope}.valuesMissing`]: 'Some values are missing',
81
106
  [`${scope}.showJSON`]: 'Show JSON',
82
107
  [`${scope}.personalizationTags`]: 'Personalization Tags',
83
108
  [`${scope}.customValues`]: 'Custom Values',
@@ -97,118 +122,128 @@ TestWrapper.propTypes = {
97
122
  };
98
123
 
99
124
  describe('CustomValuesEditor', () => {
100
- beforeEach(() => jest.clearAllMocks());
101
-
102
- describe('loading state (isExtractingTags)', () => {
103
- it('renders a spinner and extracting-tags message when isExtractingTags is true', () => {
104
- render(
105
- <TestWrapper>
106
- <CustomValuesEditor {...defaultProps} isExtractingTags />
107
- </TestWrapper>
108
- );
109
- expect(screen.getByTestId('cap-spin')).toBeTruthy();
110
- expect(screen.getByText('Extracting tags...')).toBeTruthy();
111
- });
125
+ beforeEach(() => {
126
+ jest.clearAllMocks();
127
+ });
112
128
 
113
- it('does not render the editor toggle when extracting tags', () => {
114
- render(
115
- <TestWrapper>
116
- <CustomValuesEditor {...defaultProps} isExtractingTags />
117
- </TestWrapper>
118
- );
119
- expect(screen.queryByTestId('cap-switch')).toBeNull();
120
- expect(screen.queryAllByTestId('cap-button')).toHaveLength(0);
121
- });
129
+ it('shows loading state when isExtractingTags', () => {
130
+ render(
131
+ <IntlProvider locale="en" messages={{}}>
132
+ <CustomValuesEditor {...baseProps} isExtractingTags />
133
+ </IntlProvider>,
134
+ );
135
+ expect(screen.getByTestId('cap-spin')).toBeInTheDocument();
136
+ });
122
137
 
123
- it('renders the editor (not spinner) when isExtractingTags is false', () => {
124
- render(
125
- <TestWrapper>
126
- <CustomValuesEditor {...defaultProps} isExtractingTags={false} />
127
- </TestWrapper>
128
- );
129
- expect(screen.queryByTestId('cap-spin')).toBeNull();
130
- expect(screen.getByTestId('cap-switch')).toBeTruthy();
131
- });
138
+ it('shows values missing label when isUpdatePreviewDisabled', () => {
139
+ render(
140
+ <IntlProvider locale="en" messages={{}}>
141
+ <CustomValuesEditor {...baseProps} isUpdatePreviewDisabled />
142
+ </IntlProvider>,
143
+ );
144
+ expect(document.querySelector('.values-missing-message')).toBeTruthy();
132
145
  });
133
146
 
134
- describe('values missing warning', () => {
135
- it('shows values-missing label when isUpdatePreviewDisabled is true', () => {
136
- render(
137
- <TestWrapper>
138
- <CustomValuesEditor {...defaultProps} isUpdatePreviewDisabled />
139
- </TestWrapper>
140
- );
141
- expect(screen.getByText('Values missing for some of the personalization tags')).toBeTruthy();
142
- });
147
+ it('toggles JSON mode and calls setShowJSON', () => {
148
+ const setShowJSON = jest.fn();
149
+ render(
150
+ <IntlProvider locale="en" messages={{}}>
151
+ <CustomValuesEditor {...baseProps} setShowJSON={setShowJSON} />
152
+ </IntlProvider>,
153
+ );
154
+ fireEvent.click(screen.getByTestId('json-switch'));
155
+ expect(setShowJSON).toHaveBeenCalled();
156
+ });
143
157
 
144
- it('does not show values-missing label when isUpdatePreviewDisabled is false', () => {
145
- render(
146
- <TestWrapper>
147
- <CustomValuesEditor {...defaultProps} isUpdatePreviewDisabled={false} />
148
- </TestWrapper>
149
- );
150
- expect(
151
- screen.queryByText('Values missing for some of the personalization tags')
152
- ).toBeNull();
153
- });
158
+ it('renders JSON textarea and fires handleJSONTextChange', () => {
159
+ render(
160
+ <IntlProvider locale="en" messages={{}}>
161
+ <CustomValuesEditor {...baseProps} showJSON />
162
+ </IntlProvider>,
163
+ );
164
+ const ta = document.querySelector('.json-textarea');
165
+ expect(ta).toBeTruthy();
166
+ fireEvent.change(ta, { target: { value: '{}' } });
167
+ expect(baseProps.handleJSONTextChange).toHaveBeenCalledWith('{}');
154
168
  });
155
169
 
156
- describe('JSON view (showJSON = true)', () => {
157
- it('renders a textarea with the formatted JSON', () => {
158
- const customValues = { name: 'Alice', age: 30 };
159
- render(
160
- <TestWrapper>
161
- <CustomValuesEditor {...defaultProps} showJSON customValues={customValues} />
162
- </TestWrapper>
163
- );
164
- const textarea = document.querySelector('textarea.json-textarea');
165
- expect(textarea).toBeTruthy();
166
- expect(textarea.value).toBe(JSON.stringify(customValues, null, 2));
167
- });
170
+ it('renders required and optional tag inputs when not JSON', () => {
171
+ render(
172
+ <IntlProvider locale="en" messages={{}}>
173
+ <CustomValuesEditor {...baseProps} />
174
+ </IntlProvider>,
175
+ );
176
+ const inputs = screen.getAllByTestId('tag-input');
177
+ expect(inputs.length).toBeGreaterThan(0);
178
+ fireEvent.change(inputs[0], { target: { value: 'new' } });
179
+ expect(baseProps.handleCustomValueChange).toHaveBeenCalledWith('tag.a', 'new');
180
+ });
168
181
 
169
- it('renders line numbers matching the number of JSON lines', () => {
170
- const customValues = { a: 1, b: 2 };
171
- render(
172
- <TestWrapper>
173
- <CustomValuesEditor {...defaultProps} showJSON customValues={customValues} />
174
- </TestWrapper>
175
- );
176
- const lineCount = JSON.stringify(customValues, null, 2).split('\n').length;
177
- const lineNumbers = document.querySelectorAll('.line-number');
178
- expect(lineNumbers).toHaveLength(lineCount);
179
- });
182
+ it('calls discard and update handlers', () => {
183
+ render(
184
+ <IntlProvider locale="en" messages={{}}>
185
+ <CustomValuesEditor {...baseProps} />
186
+ </IntlProvider>,
187
+ );
188
+ const buttons = screen.getAllByTestId('cap-button');
189
+ fireEvent.click(buttons[0]);
190
+ fireEvent.click(buttons[buttons.length - 1]);
191
+ expect(baseProps.handleDiscardCustomValues).toHaveBeenCalled();
192
+ expect(baseProps.handleUpdatePreview).toHaveBeenCalled();
193
+ });
180
194
 
181
- it('calls handleJSONTextChange when the textarea value changes', () => {
182
- render(
183
- <TestWrapper>
184
- <CustomValuesEditor {...defaultProps} showJSON />
185
- </TestWrapper>
186
- );
187
- const textarea = document.querySelector('textarea.json-textarea');
188
- fireEvent.change(textarea, { target: { value: '{"key": "val"}' } });
189
- expect(defaultProps.handleJSONTextChange).toHaveBeenCalledWith('{"key": "val"}');
190
- });
195
+ it('skips sections with no tags', () => {
196
+ render(
197
+ <IntlProvider locale="en" messages={{}}>
198
+ <CustomValuesEditor
199
+ {...baseProps}
200
+ sections={[
201
+ { key: 'empty', requiredTags: [], optionalTags: [] },
202
+ baseProps.sections[0],
203
+ ]}
204
+ />
205
+ </IntlProvider>,
206
+ );
207
+ expect(screen.getAllByTestId('tag-input').length).toBeGreaterThan(0);
208
+ });
191
209
 
192
- it('does not render the tag table in JSON view', () => {
193
- render(
194
- <TestWrapper>
195
- <CustomValuesEditor {...defaultProps} showJSON />
196
- </TestWrapper>
197
- );
198
- expect(screen.queryByText('Personalization Tags')).toBeNull();
199
- });
210
+ it('renders section title as FormattedMessage when title is an intl message object (not a string)', () => {
211
+ render(
212
+ <IntlProvider locale="en" messages={{ 'section.title.id': 'Intl Section Title' }}>
213
+ <CustomValuesEditor
214
+ {...baseProps}
215
+ sections={[
216
+ {
217
+ key: 'intl-sec',
218
+ title: { id: 'section.title.id', defaultMessage: 'Intl Section Title' },
219
+ requiredTags: [{ fullPath: 'tag.a', name: 'a' }],
220
+ optionalTags: [],
221
+ },
222
+ ]}
223
+ />
224
+ </IntlProvider>,
225
+ );
226
+ expect(screen.getByText('Intl Section Title')).toBeInTheDocument();
200
227
  });
201
228
 
202
- describe('tag table view (showJSON = false)', () => {
203
- it('renders Personalization Tags and Custom Values headers when extractedTags is non-empty', () => {
204
- render(
205
- <TestWrapper>
206
- <CustomValuesEditor {...defaultProps} showJSON={false} />
207
- </TestWrapper>
208
- );
209
- expect(screen.getByText('Personalization Tags')).toBeTruthy();
210
- expect(screen.getByText('Custom Values')).toBeTruthy();
211
- });
229
+ it('renders section title as plain text when title is a string', () => {
230
+ render(
231
+ <IntlProvider locale="en" messages={{}}>
232
+ <CustomValuesEditor
233
+ {...baseProps}
234
+ sections={[
235
+ {
236
+ key: 'str-sec',
237
+ title: 'Plain String Title',
238
+ requiredTags: [{ fullPath: 'tag.a', name: 'a' }],
239
+ optionalTags: [],
240
+ },
241
+ ]}
242
+ />
243
+ </IntlProvider>,
244
+ );
245
+ expect(screen.getByText('Plain String Title')).toBeInTheDocument();
246
+ });
212
247
 
213
248
  it('renders required tags with a required indicator (*)', () => {
214
249
  render(
@@ -239,124 +274,244 @@ describe('CustomValuesEditor', () => {
239
274
  expect(screen.getByText('{{customer.email}}')).toBeTruthy();
240
275
  });
241
276
 
242
- it('does not render the table when extractedTags is empty', () => {
243
- render(
244
- <TestWrapper>
245
- <CustomValuesEditor
246
- {...defaultProps}
247
- showJSON={false}
248
- extractedTags={[]}
249
- requiredTags={[]}
250
- optionalTags={[]}
251
- />
252
- </TestWrapper>
253
- );
254
- expect(screen.queryByText('Personalization Tags')).toBeNull();
255
- });
256
-
257
- it('calls handleCustomValueChange with the tag path and new value for required tags', () => {
277
+ it('calls handleCustomValueChange with the optional tag key when its input changes', () => {
258
278
  render(
259
279
  <TestWrapper>
260
280
  <CustomValuesEditor {...defaultProps} showJSON={false} />
261
281
  </TestWrapper>
262
282
  );
263
- const inputs = screen.getAllByTestId('cap-input');
264
- fireEvent.change(inputs[0], { target: { value: 'Bob' } });
265
- expect(defaultProps.handleCustomValueChange).toHaveBeenCalledWith('{{customer.name}}', 'Bob');
283
+ // requiredTags render before optionalTags, so the optional tag is the second input.
284
+ const inputs = screen.getAllByTestId('tag-input');
285
+ fireEvent.change(inputs[1], { target: { value: 'someone@test.com' } });
286
+ expect(defaultProps.handleCustomValueChange).toHaveBeenCalledWith('{{customer.email}}', 'someone@test.com');
266
287
  });
267
288
 
268
- it('calls handleCustomValueChange with the tag path and new value for optional tags', () => {
269
- render(
270
- <TestWrapper>
271
- <CustomValuesEditor {...defaultProps} showJSON={false} />
272
- </TestWrapper>
273
- );
274
- const inputs = screen.getAllByTestId('cap-input');
275
- fireEvent.change(inputs[1], { target: { value: 'bob@example.com' } });
276
- expect(defaultProps.handleCustomValueChange).toHaveBeenCalledWith(
277
- '{{customer.email}}',
278
- 'bob@example.com'
279
- );
280
- });
289
+ it('uses empty string as column label when both fullPath and name are absent', () => {
290
+ render(
291
+ <IntlProvider locale="en" messages={{}}>
292
+ <CustomValuesEditor
293
+ {...baseProps}
294
+ sections={[
295
+ {
296
+ key: 'no-label',
297
+ title: 'Section',
298
+ requiredTags: [{}],
299
+ optionalTags: [],
300
+ },
301
+ ]}
302
+ />
303
+ </IntlProvider>,
304
+ );
305
+ const inputs = screen.getAllByTestId('tag-input');
306
+ expect(inputs.length).toBeGreaterThan(0);
281
307
  });
282
308
 
283
- describe('JSON / tag toggle', () => {
284
- it('calls setShowJSON(true) when toggle is clicked from tag view', () => {
285
- render(
286
- <TestWrapper>
287
- <CustomValuesEditor {...defaultProps} showJSON={false} />
288
- </TestWrapper>
289
- );
290
- fireEvent.click(screen.getByTestId('cap-switch'));
291
- expect(defaultProps.setShowJSON).toHaveBeenCalledWith(true);
292
- });
309
+ it('renders optional-only section (no requiredTags) correctly', () => {
310
+ render(
311
+ <IntlProvider locale="en" messages={{}}>
312
+ <CustomValuesEditor
313
+ {...baseProps}
314
+ sections={[
315
+ {
316
+ key: 'optional-only',
317
+ title: 'Optional Section',
318
+ requiredTags: [],
319
+ optionalTags: [{ fullPath: 'tag.opt', name: 'opt' }],
320
+ },
321
+ ]}
322
+ />
323
+ </IntlProvider>,
324
+ );
325
+ expect(screen.getByText('Optional Section')).toBeInTheDocument();
326
+ const inputs = screen.getAllByTestId('tag-input');
327
+ expect(inputs.length).toBeGreaterThan(0);
328
+ });
293
329
 
294
- it('calls setShowJSON(false) when toggle is clicked from JSON view', () => {
295
- render(
296
- <TestWrapper>
297
- <CustomValuesEditor {...defaultProps} showJSON />
298
- </TestWrapper>
299
- );
300
- fireEvent.click(screen.getByTestId('cap-switch'));
301
- expect(defaultProps.setShowJSON).toHaveBeenCalledWith(false);
302
- });
330
+ it('renders required-only section (no optionalTags) correctly', () => {
331
+ render(
332
+ <IntlProvider locale="en" messages={{}}>
333
+ <CustomValuesEditor
334
+ {...baseProps}
335
+ sections={[
336
+ {
337
+ key: 'required-only',
338
+ title: 'Required Section',
339
+ requiredTags: [{ fullPath: 'tag.req', name: 'req' }],
340
+ optionalTags: [],
341
+ },
342
+ ]}
343
+ />
344
+ </IntlProvider>,
345
+ );
346
+ expect(screen.getByText('Required Section')).toBeInTheDocument();
347
+ const inputs = screen.getAllByTestId('tag-input');
348
+ expect(inputs.length).toBeGreaterThan(0);
303
349
  });
304
350
 
305
- describe('action buttons', () => {
306
- it('calls handleDiscardCustomValues when the discard button is clicked', () => {
307
- render(
308
- <TestWrapper>
309
- <CustomValuesEditor {...defaultProps} />
310
- </TestWrapper>
311
- );
312
- const buttons = screen.getAllByTestId('cap-button');
313
- fireEvent.click(buttons[0]); // discard is first
314
- expect(defaultProps.handleDiscardCustomValues).toHaveBeenCalled();
315
- });
351
+ it('renders line numbers in JSON mode matching JSON line count', () => {
352
+ const multiValueCustomValues = { a: 'val1', b: 'val2', c: 'val3' };
353
+ render(
354
+ <IntlProvider locale="en" messages={{}}>
355
+ <CustomValuesEditor
356
+ {...baseProps}
357
+ showJSON
358
+ customValues={multiValueCustomValues}
359
+ />
360
+ </IntlProvider>,
361
+ );
362
+ const lineNumbers = document.querySelectorAll('.line-number');
363
+ const expectedLineCount = JSON.stringify(multiValueCustomValues, null, 2).split('\n').length;
364
+ expect(lineNumbers.length).toBe(expectedLineCount);
365
+ });
316
366
 
317
- it('calls handleUpdatePreview when the update-preview button is clicked', () => {
318
- render(
319
- <TestWrapper>
320
- <CustomValuesEditor {...defaultProps} />
321
- </TestWrapper>
322
- );
323
- const buttons = screen.getAllByTestId('cap-button');
324
- fireEvent.click(buttons[1]); // update is second
325
- expect(defaultProps.handleUpdatePreview).toHaveBeenCalled();
326
- });
367
+ it('passes fullPath as key for required tag row when fullPath is present', () => {
368
+ const handleChange = jest.fn();
369
+ render(
370
+ <IntlProvider locale="en" messages={{}}>
371
+ <CustomValuesEditor
372
+ {...baseProps}
373
+ sections={[
374
+ {
375
+ key: 'sec',
376
+ title: 'S',
377
+ requiredTags: [{ fullPath: 'my.full.path', name: 'name' }],
378
+ optionalTags: [],
379
+ },
380
+ ]}
381
+ handleCustomValueChange={handleChange}
382
+ customValues={{ 'my.full.path': 'existing' }}
383
+ />
384
+ </IntlProvider>,
385
+ );
386
+ const input = screen.getByTestId('tag-input');
387
+ expect(input.value).toBe('existing');
388
+ fireEvent.change(input, { target: { value: 'updated' } });
389
+ expect(handleChange).toHaveBeenCalledWith('my.full.path', 'updated');
390
+ });
327
391
 
328
- it('disables the update-preview button when isUpdatePreviewDisabled is true', () => {
329
- render(
330
- <TestWrapper>
331
- <CustomValuesEditor {...defaultProps} isUpdatePreviewDisabled />
332
- </TestWrapper>
333
- );
334
- const buttons = screen.getAllByTestId('cap-button');
335
- expect(buttons[1].disabled).toBe(true);
336
- });
392
+ it('renders no sections when sections is undefined (sections || [] fallback)', () => {
393
+ render(
394
+ <IntlProvider locale="en" messages={{}}>
395
+ <CustomValuesEditor {...baseProps} sections={undefined} />
396
+ </IntlProvider>,
397
+ );
398
+ expect(screen.queryAllByTestId('tag-input').length).toBe(0);
399
+ });
337
400
 
338
- it('disables the update-preview button when isUpdatingPreview is true', () => {
339
- render(
340
- <TestWrapper>
341
- <CustomValuesEditor {...defaultProps} isUpdatingPreview />
342
- </TestWrapper>
343
- );
344
- const buttons = screen.getAllByTestId('cap-button');
345
- expect(buttons[1].disabled).toBe(true);
346
- });
401
+ it('falls back to title.id for the section key when key is absent but title is a message descriptor', () => {
402
+ render(
403
+ <IntlProvider locale="en" messages={{ 'title.id.value': 'Descriptor Title' }}>
404
+ <CustomValuesEditor
405
+ {...baseProps}
406
+ sections={[
407
+ {
408
+ title: { id: 'title.id.value', defaultMessage: 'Descriptor Title' },
409
+ requiredTags: [{ fullPath: 'tag.x', name: 'x' }],
410
+ optionalTags: [],
411
+ },
412
+ ]}
413
+ />
414
+ </IntlProvider>,
415
+ );
416
+ expect(screen.getByText('Descriptor Title')).toBeInTheDocument();
417
+ });
347
418
 
348
- it('enables the update-preview button when neither flag is set', () => {
349
- render(
350
- <TestWrapper>
351
- <CustomValuesEditor
352
- {...defaultProps}
353
- isUpdatePreviewDisabled={false}
354
- isUpdatingPreview={false}
355
- />
356
- </TestWrapper>
357
- );
358
- const buttons = screen.getAllByTestId('cap-button');
359
- expect(buttons[1].disabled).toBe(false);
360
- });
419
+ it('falls back to the title string for the section key when key is absent and title has no id', () => {
420
+ render(
421
+ <IntlProvider locale="en" messages={{}}>
422
+ <CustomValuesEditor
423
+ {...baseProps}
424
+ sections={[
425
+ {
426
+ title: 'Keyless Section Title',
427
+ requiredTags: [{ fullPath: 'tag.y', name: 'y' }],
428
+ optionalTags: [],
429
+ },
430
+ ]}
431
+ />
432
+ </IntlProvider>,
433
+ );
434
+ expect(screen.getByText('Keyless Section Title')).toBeInTheDocument();
435
+ });
436
+
437
+ it('falls back to the fallback constant key and renders no title label when key and title are both absent', () => {
438
+ render(
439
+ <IntlProvider locale="en" messages={{}}>
440
+ <CustomValuesEditor
441
+ {...baseProps}
442
+ sections={[
443
+ {
444
+ requiredTags: [{ fullPath: 'tag.z', name: 'z' }],
445
+ optionalTags: [],
446
+ },
447
+ ]}
448
+ />
449
+ </IntlProvider>,
450
+ );
451
+ const sectionTitleLabels = screen.queryAllByTestId('cap-label')
452
+ .filter((el) => el.className === 'tags-section-title');
453
+ expect(sectionTitleLabels.length).toBe(0);
454
+ expect(screen.getAllByTestId('tag-input').length).toBeGreaterThan(0);
455
+ });
456
+
457
+ it('falls back to an empty array when requiredTags key is absent from the section', () => {
458
+ render(
459
+ <IntlProvider locale="en" messages={{}}>
460
+ <CustomValuesEditor
461
+ {...baseProps}
462
+ sections={[
463
+ {
464
+ key: 'no-required-tags-key',
465
+ title: 'No RequiredTags Key',
466
+ optionalTags: [{ fullPath: 'tag.opt-only', name: 'opt-only' }],
467
+ },
468
+ ]}
469
+ />
470
+ </IntlProvider>,
471
+ );
472
+ expect(screen.getByText('No RequiredTags Key')).toBeInTheDocument();
473
+ expect(screen.getAllByTestId('tag-input').length).toBe(1);
474
+ });
475
+
476
+ it('falls back to an empty array when optionalTags key is absent from the section', () => {
477
+ render(
478
+ <IntlProvider locale="en" messages={{}}>
479
+ <CustomValuesEditor
480
+ {...baseProps}
481
+ sections={[
482
+ {
483
+ key: 'no-optional-tags-key',
484
+ title: 'No OptionalTags Key',
485
+ requiredTags: [{ fullPath: 'tag.req-only', name: 'req-only' }],
486
+ },
487
+ ]}
488
+ />
489
+ </IntlProvider>,
490
+ );
491
+ expect(screen.getByText('No OptionalTags Key')).toBeInTheDocument();
492
+ expect(screen.getAllByTestId('tag-input').length).toBe(1);
493
+ });
494
+
495
+ it('uses an index-based fallback key for optional tag rows when fullPath is absent', () => {
496
+ const handleChange = jest.fn();
497
+ render(
498
+ <IntlProvider locale="en" messages={{}}>
499
+ <CustomValuesEditor
500
+ {...baseProps}
501
+ sections={[
502
+ {
503
+ key: 'opt-no-fullpath',
504
+ title: 'Optional No FullPath',
505
+ requiredTags: [],
506
+ optionalTags: [{ name: 'noPathOpt' }],
507
+ },
508
+ ]}
509
+ handleCustomValueChange={handleChange}
510
+ />
511
+ </IntlProvider>,
512
+ );
513
+ const input = screen.getByTestId('tag-input');
514
+ fireEvent.change(input, { target: { value: 'val' } });
515
+ expect(handleChange).toHaveBeenCalledWith('optional-0', 'val');
361
516
  });
362
517
  });