@capillarytech/creatives-library 8.0.345-alpha.13 → 8.0.345-alpha.15

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 (138) hide show
  1. package/constants/unified.js +29 -0
  2. package/package.json +1 -1
  3. package/services/api.js +0 -20
  4. package/services/tests/api.test.js +13 -59
  5. package/utils/commonUtils.js +19 -1
  6. package/utils/rcsPayloadUtils.js +92 -0
  7. package/utils/templateVarUtils.js +201 -0
  8. package/utils/tests/templateVarUtils.test.js +204 -0
  9. package/v2Components/CapActionButton/constants.js +7 -0
  10. package/v2Components/CapActionButton/index.js +167 -109
  11. package/v2Components/CapActionButton/index.scss +157 -6
  12. package/v2Components/CapActionButton/messages.js +19 -3
  13. package/v2Components/CapActionButton/tests/index.test.js +41 -17
  14. package/v2Components/CapCustomSkeleton/index.js +1 -1
  15. package/v2Components/CapCustomSkeleton/tests/__snapshots__/index.test.js.snap +12 -12
  16. package/v2Components/CapTagList/index.js +10 -0
  17. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  23. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  24. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +160 -15
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js.rej +18 -0
  27. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +341 -76
  28. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  29. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  30. package/v2Components/CommonTestAndPreview/constants.js +38 -2
  31. package/v2Components/CommonTestAndPreview/index.js +676 -186
  32. package/v2Components/CommonTestAndPreview/messages.js +49 -3
  33. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  34. package/v2Components/CommonTestAndPreview/sagas.js +15 -6
  35. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +308 -284
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  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 +34 -13
  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 +132 -4
  44. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  45. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
  46. package/v2Components/FormBuilder/index.js +8 -10
  47. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
  48. package/v2Components/SmsFallback/constants.js +73 -0
  49. package/v2Components/SmsFallback/index.js +955 -0
  50. package/v2Components/SmsFallback/index.scss +265 -0
  51. package/v2Components/SmsFallback/messages.js +78 -0
  52. package/v2Components/SmsFallback/smsFallbackUtils.js +118 -0
  53. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  54. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  55. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  56. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
  57. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +277 -0
  58. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  59. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  60. package/v2Components/TemplatePreview/_templatePreview.scss +33 -23
  61. package/v2Components/TemplatePreview/constants.js +2 -0
  62. package/v2Components/TemplatePreview/index.js +143 -28
  63. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  64. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  65. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  66. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  67. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  68. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  69. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  70. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  71. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  72. package/v2Containers/CreativesContainer/SlideBoxFooter.js +11 -4
  73. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  74. package/v2Containers/CreativesContainer/constants.js +9 -0
  75. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
  76. package/v2Containers/CreativesContainer/index.js +300 -108
  77. package/v2Containers/CreativesContainer/index.scss +51 -1
  78. package/v2Containers/CreativesContainer/messages.js +0 -4
  79. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  80. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +78 -34
  81. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  82. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  83. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +357 -98
  84. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -18
  85. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  86. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  87. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  88. package/v2Containers/Rcs/constants.js +119 -8
  89. package/v2Containers/Rcs/index.js +2379 -807
  90. package/v2Containers/Rcs/index.js.rej +1336 -0
  91. package/v2Containers/Rcs/index.scss +276 -6
  92. package/v2Containers/Rcs/index.scss.rej +74 -0
  93. package/v2Containers/Rcs/messages.js +38 -3
  94. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +225 -0
  95. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +98018 -70073
  96. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  97. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap.rej +128 -0
  98. package/v2Containers/Rcs/tests/index.test.js +152 -121
  99. package/v2Containers/Rcs/tests/mockData.js +38 -0
  100. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  101. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  102. package/v2Containers/Rcs/utils.js +478 -11
  103. package/v2Containers/Sms/Create/index.js +100 -40
  104. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  105. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  106. package/v2Containers/SmsTrai/Create/index.js +9 -4
  107. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  108. package/v2Containers/SmsTrai/Edit/index.js +636 -130
  109. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  110. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  111. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4328 -2375
  112. package/v2Containers/SmsWrapper/index.js +37 -8
  113. package/v2Containers/TagList/index.js +6 -0
  114. package/v2Containers/Templates/ChannelTypeIllustration.js +6 -23
  115. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  116. package/v2Containers/Templates/_templates.scss +181 -126
  117. package/v2Containers/Templates/actions.js +11 -36
  118. package/v2Containers/Templates/constants.js +2 -23
  119. package/v2Containers/Templates/index.js +142 -333
  120. package/v2Containers/Templates/messages.js +0 -68
  121. package/v2Containers/Templates/reducer.js +0 -68
  122. package/v2Containers/Templates/sagas.js +55 -98
  123. package/v2Containers/Templates/selectors.js +0 -12
  124. package/v2Containers/Templates/tests/ChannelTypeIllustration.test.js +0 -12
  125. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  126. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1042 -1256
  127. package/v2Containers/Templates/tests/index.test.js +0 -6
  128. package/v2Containers/Templates/tests/reducer.test.js +0 -178
  129. package/v2Containers/Templates/tests/sagas.test.js +200 -436
  130. package/v2Containers/Templates/tests/selector.test.js +0 -32
  131. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  132. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  133. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  134. package/v2Containers/TemplatesV2/index.js +86 -23
  135. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  136. package/v2Containers/Whatsapp/index.js +3 -20
  137. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
  138. package/v2Containers/Assets/images/archive_Empty_Illustration.svg +0 -9
@@ -1,83 +1,95 @@
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,
74
86
  };
75
87
 
76
88
  // IntlProvider wrapper for incoming tests
77
89
  const scope = 'app.v2Components.TestAndPreviewSlidebox';
78
90
  const mockMessages = {
79
91
  [`${scope}.extractingTags`]: 'Extracting tags...',
80
- [`${scope}.valuesMissing`]: 'Values missing for some of the personalization tags',
92
+ [`${scope}.valuesMissing`]: 'Some values are missing',
81
93
  [`${scope}.showJSON`]: 'Show JSON',
82
94
  [`${scope}.personalizationTags`]: 'Personalization Tags',
83
95
  [`${scope}.customValues`]: 'Custom Values',
@@ -97,256 +109,268 @@ TestWrapper.propTypes = {
97
109
  };
98
110
 
99
111
  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
- });
112
-
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
- });
122
-
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
- });
112
+ beforeEach(() => {
113
+ jest.clearAllMocks();
132
114
  });
133
115
 
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
- });
143
-
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
- });
116
+ it('shows loading state when isExtractingTags', () => {
117
+ render(
118
+ <IntlProvider locale="en" messages={{}}>
119
+ <CustomValuesEditor {...baseProps} isExtractingTags />
120
+ </IntlProvider>,
121
+ );
122
+ expect(screen.getByTestId('cap-spin')).toBeInTheDocument();
154
123
  });
155
124
 
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
- });
168
-
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
- });
180
-
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
- });
125
+ it('shows values missing label when isUpdatePreviewDisabled', () => {
126
+ render(
127
+ <IntlProvider locale="en" messages={{}}>
128
+ <CustomValuesEditor {...baseProps} isUpdatePreviewDisabled />
129
+ </IntlProvider>,
130
+ );
131
+ expect(document.querySelector('.values-missing-message')).toBeTruthy();
132
+ });
191
133
 
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
- });
134
+ it('toggles JSON mode and calls setShowJSON', () => {
135
+ const setShowJSON = jest.fn();
136
+ render(
137
+ <IntlProvider locale="en" messages={{}}>
138
+ <CustomValuesEditor {...baseProps} setShowJSON={setShowJSON} />
139
+ </IntlProvider>,
140
+ );
141
+ fireEvent.click(screen.getByTestId('json-switch'));
142
+ expect(setShowJSON).toHaveBeenCalled();
200
143
  });
201
144
 
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
- });
145
+ it('renders JSON textarea and fires handleJSONTextChange', () => {
146
+ render(
147
+ <IntlProvider locale="en" messages={{}}>
148
+ <CustomValuesEditor {...baseProps} showJSON />
149
+ </IntlProvider>,
150
+ );
151
+ const ta = document.querySelector('.json-textarea');
152
+ expect(ta).toBeTruthy();
153
+ fireEvent.change(ta, { target: { value: '{}' } });
154
+ expect(baseProps.handleJSONTextChange).toHaveBeenCalledWith('{}');
155
+ });
212
156
 
213
- it('renders required tags with a required indicator (*)', () => {
214
- render(
215
- <TestWrapper>
216
- <CustomValuesEditor {...defaultProps} showJSON={false} />
217
- </TestWrapper>
218
- );
219
- expect(screen.getByText('{{customer.name}}')).toBeTruthy();
220
- expect(screen.getByText('*')).toBeTruthy();
221
- });
157
+ it('renders required and optional tag inputs when not JSON', () => {
158
+ render(
159
+ <IntlProvider locale="en" messages={{}}>
160
+ <CustomValuesEditor {...baseProps} />
161
+ </IntlProvider>,
162
+ );
163
+ const inputs = screen.getAllByTestId('tag-input');
164
+ expect(inputs.length).toBeGreaterThan(0);
165
+ fireEvent.change(inputs[0], { target: { value: 'new' } });
166
+ expect(baseProps.handleCustomValueChange).toHaveBeenCalledWith('tag.a', 'new');
167
+ });
222
168
 
223
- it('renders optional tags without a required indicator', () => {
224
- render(
225
- <TestWrapper>
226
- <CustomValuesEditor {...defaultProps} showJSON={false} />
227
- </TestWrapper>
228
- );
229
- expect(screen.getByText('{{customer.email}}')).toBeTruthy();
230
- });
169
+ it('calls discard and update handlers', () => {
170
+ render(
171
+ <IntlProvider locale="en" messages={{}}>
172
+ <CustomValuesEditor {...baseProps} />
173
+ </IntlProvider>,
174
+ );
175
+ const buttons = screen.getAllByTestId('cap-button');
176
+ fireEvent.click(buttons[0]);
177
+ fireEvent.click(buttons[buttons.length - 1]);
178
+ expect(baseProps.handleDiscardCustomValues).toHaveBeenCalled();
179
+ expect(baseProps.handleUpdatePreview).toHaveBeenCalled();
180
+ });
231
181
 
232
- it('does not render the table when extractedTags is empty', () => {
233
- render(
234
- <TestWrapper>
235
- <CustomValuesEditor
236
- {...defaultProps}
237
- showJSON={false}
238
- extractedTags={[]}
239
- requiredTags={[]}
240
- optionalTags={[]}
241
- />
242
- </TestWrapper>
243
- );
244
- expect(screen.queryByText('Personalization Tags')).toBeNull();
245
- });
182
+ it('skips sections with no tags', () => {
183
+ render(
184
+ <IntlProvider locale="en" messages={{}}>
185
+ <CustomValuesEditor
186
+ {...baseProps}
187
+ sections={[
188
+ { key: 'empty', requiredTags: [], optionalTags: [] },
189
+ baseProps.sections[0],
190
+ ]}
191
+ />
192
+ </IntlProvider>,
193
+ );
194
+ expect(screen.getAllByTestId('tag-input').length).toBeGreaterThan(0);
195
+ });
246
196
 
247
- it('calls handleCustomValueChange with the tag path and new value for required tags', () => {
248
- render(
249
- <TestWrapper>
250
- <CustomValuesEditor {...defaultProps} showJSON={false} />
251
- </TestWrapper>
252
- );
253
- const inputs = screen.getAllByTestId('cap-input');
254
- fireEvent.change(inputs[0], { target: { value: 'Bob' } });
255
- expect(defaultProps.handleCustomValueChange).toHaveBeenCalledWith('{{customer.name}}', 'Bob');
256
- });
197
+ it('renders section title as FormattedMessage when title is an intl message object (not a string)', () => {
198
+ render(
199
+ <IntlProvider locale="en" messages={{ 'section.title.id': 'Intl Section Title' }}>
200
+ <CustomValuesEditor
201
+ {...baseProps}
202
+ sections={[
203
+ {
204
+ key: 'intl-sec',
205
+ title: { id: 'section.title.id', defaultMessage: 'Intl Section Title' },
206
+ requiredTags: [{ fullPath: 'tag.a', name: 'a' }],
207
+ optionalTags: [],
208
+ },
209
+ ]}
210
+ />
211
+ </IntlProvider>,
212
+ );
213
+ expect(screen.getByText('Intl Section Title')).toBeInTheDocument();
214
+ });
257
215
 
258
- it('calls handleCustomValueChange with the tag path and new value for optional tags', () => {
259
- render(
260
- <TestWrapper>
261
- <CustomValuesEditor {...defaultProps} showJSON={false} />
262
- </TestWrapper>
263
- );
264
- const inputs = screen.getAllByTestId('cap-input');
265
- fireEvent.change(inputs[1], { target: { value: 'bob@example.com' } });
266
- expect(defaultProps.handleCustomValueChange).toHaveBeenCalledWith(
267
- '{{customer.email}}',
268
- 'bob@example.com'
269
- );
270
- });
216
+ it('renders section title as plain text when title is a string', () => {
217
+ render(
218
+ <IntlProvider locale="en" messages={{}}>
219
+ <CustomValuesEditor
220
+ {...baseProps}
221
+ sections={[
222
+ {
223
+ key: 'str-sec',
224
+ title: 'Plain String Title',
225
+ requiredTags: [{ fullPath: 'tag.a', name: 'a' }],
226
+ optionalTags: [],
227
+ },
228
+ ]}
229
+ />
230
+ </IntlProvider>,
231
+ );
232
+ expect(screen.getByText('Plain String Title')).toBeInTheDocument();
271
233
  });
272
234
 
273
- describe('JSON / tag toggle', () => {
274
- it('calls setShowJSON(true) when toggle is clicked from tag view', () => {
275
- render(
276
- <TestWrapper>
277
- <CustomValuesEditor {...defaultProps} showJSON={false} />
278
- </TestWrapper>
279
- );
280
- fireEvent.click(screen.getByTestId('cap-switch'));
281
- expect(defaultProps.setShowJSON).toHaveBeenCalledWith(true);
282
- });
235
+ it('renders nothing for section title when title is null (does not crash)', () => {
236
+ render(
237
+ <IntlProvider locale="en" messages={{}}>
238
+ <CustomValuesEditor
239
+ {...baseProps}
240
+ sections={[
241
+ {
242
+ key: 'no-title-sec',
243
+ title: null,
244
+ requiredTags: [{ fullPath: 'tag.a', name: 'a' }],
245
+ optionalTags: [],
246
+ },
247
+ ]}
248
+ />
249
+ </IntlProvider>,
250
+ );
251
+ const inputs = screen.getAllByTestId('tag-input');
252
+ expect(inputs.length).toBeGreaterThan(0);
253
+ });
283
254
 
284
- it('calls setShowJSON(false) when toggle is clicked from JSON view', () => {
285
- render(
286
- <TestWrapper>
287
- <CustomValuesEditor {...defaultProps} showJSON />
288
- </TestWrapper>
289
- );
290
- fireEvent.click(screen.getByTestId('cap-switch'));
291
- expect(defaultProps.setShowJSON).toHaveBeenCalledWith(false);
292
- });
255
+ it('uses tag.name as column label when fullPath is absent', () => {
256
+ render(
257
+ <IntlProvider locale="en" messages={{}}>
258
+ <CustomValuesEditor
259
+ {...baseProps}
260
+ sections={[
261
+ {
262
+ key: 'name-only',
263
+ title: 'Section',
264
+ requiredTags: [{ name: 'myTag' }],
265
+ optionalTags: [],
266
+ },
267
+ ]}
268
+ />
269
+ </IntlProvider>,
270
+ );
271
+ expect(screen.getByText('myTag')).toBeInTheDocument();
293
272
  });
294
273
 
295
- describe('action buttons', () => {
296
- it('calls handleDiscardCustomValues when the discard button is clicked', () => {
297
- render(
298
- <TestWrapper>
299
- <CustomValuesEditor {...defaultProps} />
300
- </TestWrapper>
301
- );
302
- const buttons = screen.getAllByTestId('cap-button');
303
- fireEvent.click(buttons[0]); // discard is first
304
- expect(defaultProps.handleDiscardCustomValues).toHaveBeenCalled();
305
- });
274
+ it('uses empty string as column label when both fullPath and name are absent', () => {
275
+ render(
276
+ <IntlProvider locale="en" messages={{}}>
277
+ <CustomValuesEditor
278
+ {...baseProps}
279
+ sections={[
280
+ {
281
+ key: 'no-label',
282
+ title: 'Section',
283
+ requiredTags: [{}],
284
+ optionalTags: [],
285
+ },
286
+ ]}
287
+ />
288
+ </IntlProvider>,
289
+ );
290
+ const inputs = screen.getAllByTestId('tag-input');
291
+ expect(inputs.length).toBeGreaterThan(0);
292
+ });
306
293
 
307
- it('calls handleUpdatePreview when the update-preview button is clicked', () => {
308
- render(
309
- <TestWrapper>
310
- <CustomValuesEditor {...defaultProps} />
311
- </TestWrapper>
312
- );
313
- const buttons = screen.getAllByTestId('cap-button');
314
- fireEvent.click(buttons[1]); // update is second
315
- expect(defaultProps.handleUpdatePreview).toHaveBeenCalled();
316
- });
294
+ it('renders optional-only section (no requiredTags) correctly', () => {
295
+ render(
296
+ <IntlProvider locale="en" messages={{}}>
297
+ <CustomValuesEditor
298
+ {...baseProps}
299
+ sections={[
300
+ {
301
+ key: 'optional-only',
302
+ title: 'Optional Section',
303
+ requiredTags: [],
304
+ optionalTags: [{ fullPath: 'tag.opt', name: 'opt' }],
305
+ },
306
+ ]}
307
+ />
308
+ </IntlProvider>,
309
+ );
310
+ expect(screen.getByText('Optional Section')).toBeInTheDocument();
311
+ const inputs = screen.getAllByTestId('tag-input');
312
+ expect(inputs.length).toBeGreaterThan(0);
313
+ });
317
314
 
318
- it('disables the update-preview button when isUpdatePreviewDisabled is true', () => {
319
- render(
320
- <TestWrapper>
321
- <CustomValuesEditor {...defaultProps} isUpdatePreviewDisabled />
322
- </TestWrapper>
323
- );
324
- const buttons = screen.getAllByTestId('cap-button');
325
- expect(buttons[1].disabled).toBe(true);
326
- });
315
+ it('renders required-only section (no optionalTags) correctly', () => {
316
+ render(
317
+ <IntlProvider locale="en" messages={{}}>
318
+ <CustomValuesEditor
319
+ {...baseProps}
320
+ sections={[
321
+ {
322
+ key: 'required-only',
323
+ title: 'Required Section',
324
+ requiredTags: [{ fullPath: 'tag.req', name: 'req' }],
325
+ optionalTags: [],
326
+ },
327
+ ]}
328
+ />
329
+ </IntlProvider>,
330
+ );
331
+ expect(screen.getByText('Required Section')).toBeInTheDocument();
332
+ const inputs = screen.getAllByTestId('tag-input');
333
+ expect(inputs.length).toBeGreaterThan(0);
334
+ });
327
335
 
328
- it('disables the update-preview button when isUpdatingPreview is true', () => {
329
- render(
330
- <TestWrapper>
331
- <CustomValuesEditor {...defaultProps} isUpdatingPreview />
332
- </TestWrapper>
333
- );
334
- const buttons = screen.getAllByTestId('cap-button');
335
- expect(buttons[1].disabled).toBe(true);
336
- });
336
+ it('renders line numbers in JSON mode matching JSON line count', () => {
337
+ const multiValueCustomValues = { a: 'val1', b: 'val2', c: 'val3' };
338
+ render(
339
+ <IntlProvider locale="en" messages={{}}>
340
+ <CustomValuesEditor
341
+ {...baseProps}
342
+ showJSON
343
+ customValues={multiValueCustomValues}
344
+ />
345
+ </IntlProvider>,
346
+ );
347
+ const lineNumbers = document.querySelectorAll('.line-number');
348
+ const expectedLineCount = JSON.stringify(multiValueCustomValues, null, 2).split('\n').length;
349
+ expect(lineNumbers.length).toBe(expectedLineCount);
350
+ });
337
351
 
338
- it('enables the update-preview button when neither flag is set', () => {
339
- render(
340
- <TestWrapper>
341
- <CustomValuesEditor
342
- {...defaultProps}
343
- isUpdatePreviewDisabled={false}
344
- isUpdatingPreview={false}
345
- />
346
- </TestWrapper>
347
- );
348
- const buttons = screen.getAllByTestId('cap-button');
349
- expect(buttons[1].disabled).toBe(false);
350
- });
352
+ it('passes fullPath as key for required tag row when fullPath is present', () => {
353
+ const handleChange = jest.fn();
354
+ render(
355
+ <IntlProvider locale="en" messages={{}}>
356
+ <CustomValuesEditor
357
+ {...baseProps}
358
+ sections={[
359
+ {
360
+ key: 'sec',
361
+ title: 'S',
362
+ requiredTags: [{ fullPath: 'my.full.path', name: 'name' }],
363
+ optionalTags: [],
364
+ },
365
+ ]}
366
+ handleCustomValueChange={handleChange}
367
+ customValues={{ 'my.full.path': 'existing' }}
368
+ />
369
+ </IntlProvider>,
370
+ );
371
+ const input = screen.getByTestId('tag-input');
372
+ expect(input.value).toBe('existing');
373
+ fireEvent.change(input, { target: { value: 'updated' } });
374
+ expect(handleChange).toHaveBeenCalledWith('my.full.path', 'updated');
351
375
  });
352
376
  });