@capillarytech/creatives-library 9.0.15-alpha.0 → 9.0.15-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 (132) 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 +19 -1
  6. package/utils/rcsPayloadUtils.js +92 -0
  7. package/utils/templateVarUtils.js +201 -0
  8. package/utils/tests/rcsPayloadUtils.test.js +226 -0
  9. package/utils/tests/templateVarUtils.test.js +204 -0
  10. package/v2Components/CapActionButton/constants.js +7 -0
  11. package/v2Components/CapActionButton/index.js +168 -109
  12. package/v2Components/CapActionButton/index.scss +156 -5
  13. package/v2Components/CapActionButton/messages.js +19 -3
  14. package/v2Components/CapActionButton/tests/index.test.js +42 -18
  15. package/v2Components/CapImageUpload/index.js +2 -2
  16. package/v2Components/CapTagList/index.js +10 -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 +60 -11
  24. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +201 -23
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +355 -76
  27. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +155 -8
  28. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  29. package/v2Components/CommonTestAndPreview/constants.js +38 -2
  30. package/v2Components/CommonTestAndPreview/index.js +761 -222
  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 +284 -267
  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 +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/utils.js +84 -0
  47. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  48. package/v2Components/SmsFallback/constants.js +94 -0
  49. package/v2Components/SmsFallback/index.js +958 -0
  50. package/v2Components/SmsFallback/index.scss +266 -0
  51. package/v2Components/SmsFallback/messages.js +78 -0
  52. package/v2Components/SmsFallback/smsFallbackUtils.js +120 -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 +208 -0
  57. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -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 +41 -22
  61. package/v2Components/TemplatePreview/constants.js +2 -0
  62. package/v2Components/TemplatePreview/index.js +143 -31
  63. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  64. package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +6 -6
  65. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  66. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  67. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  68. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  69. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  70. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  71. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +4 -1
  72. package/v2Containers/CommunicationFlow/index.js +3 -3
  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 +17 -0
  76. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -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 +79 -0
  81. package/v2Containers/CreativesContainer/index.js +323 -104
  82. package/v2Containers/CreativesContainer/index.scss +83 -1
  83. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  84. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +79 -34
  85. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  86. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  87. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +333 -90
  88. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  89. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  90. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  91. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  92. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  93. package/v2Containers/Rcs/constants.js +131 -11
  94. package/v2Containers/Rcs/index.js +2602 -807
  95. package/v2Containers/Rcs/index.scss +289 -8
  96. package/v2Containers/Rcs/messages.js +34 -3
  97. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +227 -0
  98. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +73535 -38537
  99. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -8
  100. package/v2Containers/Rcs/tests/index.test.js +147 -36
  101. package/v2Containers/Rcs/tests/mockData.js +38 -0
  102. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  103. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  104. package/v2Containers/Rcs/utils.js +478 -11
  105. package/v2Containers/Sms/Create/index.js +115 -48
  106. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  107. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  108. package/v2Containers/SmsTrai/Create/index.js +9 -4
  109. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  110. package/v2Containers/SmsTrai/Edit/index.js +678 -169
  111. package/v2Containers/SmsTrai/Edit/index.scss +126 -0
  112. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  113. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
  114. package/v2Containers/SmsWrapper/index.js +37 -8
  115. package/v2Containers/TagList/index.js +6 -0
  116. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  117. package/v2Containers/Templates/_templates.scss +171 -12
  118. package/v2Containers/Templates/actions.js +11 -0
  119. package/v2Containers/Templates/constants.js +2 -0
  120. package/v2Containers/Templates/index.js +125 -55
  121. package/v2Containers/Templates/sagas.js +57 -13
  122. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  123. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
  124. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  125. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  126. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  127. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  128. package/v2Containers/TemplatesV2/index.js +86 -23
  129. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  130. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  131. package/v2Containers/Whatsapp/index.js +3 -20
  132. 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,106 @@ 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', () => {
258
- render(
259
- <TestWrapper>
260
- <CustomValuesEditor {...defaultProps} showJSON={false} />
261
- </TestWrapper>
262
- );
263
- const inputs = screen.getAllByTestId('cap-input');
264
- fireEvent.change(inputs[0], { target: { value: 'Bob' } });
265
- expect(defaultProps.handleCustomValueChange).toHaveBeenCalledWith('{{customer.name}}', 'Bob');
266
- });
267
-
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
- });
277
+ it('uses empty string as column label when both fullPath and name are absent', () => {
278
+ render(
279
+ <IntlProvider locale="en" messages={{}}>
280
+ <CustomValuesEditor
281
+ {...baseProps}
282
+ sections={[
283
+ {
284
+ key: 'no-label',
285
+ title: 'Section',
286
+ requiredTags: [{}],
287
+ optionalTags: [],
288
+ },
289
+ ]}
290
+ />
291
+ </IntlProvider>,
292
+ );
293
+ const inputs = screen.getAllByTestId('tag-input');
294
+ expect(inputs.length).toBeGreaterThan(0);
281
295
  });
282
296
 
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
- });
293
-
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
- });
297
+ it('renders optional-only section (no requiredTags) correctly', () => {
298
+ render(
299
+ <IntlProvider locale="en" messages={{}}>
300
+ <CustomValuesEditor
301
+ {...baseProps}
302
+ sections={[
303
+ {
304
+ key: 'optional-only',
305
+ title: 'Optional Section',
306
+ requiredTags: [],
307
+ optionalTags: [{ fullPath: 'tag.opt', name: 'opt' }],
308
+ },
309
+ ]}
310
+ />
311
+ </IntlProvider>,
312
+ );
313
+ expect(screen.getByText('Optional Section')).toBeInTheDocument();
314
+ const inputs = screen.getAllByTestId('tag-input');
315
+ expect(inputs.length).toBeGreaterThan(0);
303
316
  });
304
317
 
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
- });
316
-
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
- });
327
-
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
- });
318
+ it('renders required-only section (no optionalTags) correctly', () => {
319
+ render(
320
+ <IntlProvider locale="en" messages={{}}>
321
+ <CustomValuesEditor
322
+ {...baseProps}
323
+ sections={[
324
+ {
325
+ key: 'required-only',
326
+ title: 'Required Section',
327
+ requiredTags: [{ fullPath: 'tag.req', name: 'req' }],
328
+ optionalTags: [],
329
+ },
330
+ ]}
331
+ />
332
+ </IntlProvider>,
333
+ );
334
+ expect(screen.getByText('Required Section')).toBeInTheDocument();
335
+ const inputs = screen.getAllByTestId('tag-input');
336
+ expect(inputs.length).toBeGreaterThan(0);
337
+ });
337
338
 
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
- });
339
+ it('renders line numbers in JSON mode matching JSON line count', () => {
340
+ const multiValueCustomValues = { a: 'val1', b: 'val2', c: 'val3' };
341
+ render(
342
+ <IntlProvider locale="en" messages={{}}>
343
+ <CustomValuesEditor
344
+ {...baseProps}
345
+ showJSON
346
+ customValues={multiValueCustomValues}
347
+ />
348
+ </IntlProvider>,
349
+ );
350
+ const lineNumbers = document.querySelectorAll('.line-number');
351
+ const expectedLineCount = JSON.stringify(multiValueCustomValues, null, 2).split('\n').length;
352
+ expect(lineNumbers.length).toBe(expectedLineCount);
353
+ });
347
354
 
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
- });
355
+ it('passes fullPath as key for required tag row when fullPath is present', () => {
356
+ const handleChange = jest.fn();
357
+ render(
358
+ <IntlProvider locale="en" messages={{}}>
359
+ <CustomValuesEditor
360
+ {...baseProps}
361
+ sections={[
362
+ {
363
+ key: 'sec',
364
+ title: 'S',
365
+ requiredTags: [{ fullPath: 'my.full.path', name: 'name' }],
366
+ optionalTags: [],
367
+ },
368
+ ]}
369
+ handleCustomValueChange={handleChange}
370
+ customValues={{ 'my.full.path': 'existing' }}
371
+ />
372
+ </IntlProvider>,
373
+ );
374
+ const input = screen.getByTestId('tag-input');
375
+ expect(input.value).toBe('existing');
376
+ fireEvent.change(input, { target: { value: 'updated' } });
377
+ expect(handleChange).toHaveBeenCalledWith('my.full.path', 'updated');
361
378
  });
362
379
  });