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

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 +0 -29
  2. package/package.json +1 -1
  3. package/services/api.js +20 -0
  4. package/services/tests/api.test.js +59 -13
  5. package/utils/commonUtils.js +1 -19
  6. package/v2Components/CapActionButton/constants.js +0 -7
  7. package/v2Components/CapActionButton/index.js +109 -167
  8. package/v2Components/CapActionButton/index.scss +6 -157
  9. package/v2Components/CapActionButton/messages.js +3 -19
  10. package/v2Components/CapActionButton/tests/index.test.js +17 -41
  11. package/v2Components/CapCustomSkeleton/index.js +1 -1
  12. package/v2Components/CapCustomSkeleton/tests/__snapshots__/index.test.js.snap +12 -12
  13. package/v2Components/CapTagList/index.js +0 -10
  14. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +49 -70
  15. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +2 -8
  16. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +21 -207
  17. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +0 -16
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +10 -85
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +0 -30
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +11 -79
  21. package/v2Components/CommonTestAndPreview/SendTestMessage.js +5 -10
  22. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +15 -160
  23. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +76 -341
  24. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +4 -133
  25. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +0 -11
  26. package/v2Components/CommonTestAndPreview/constants.js +2 -38
  27. package/v2Components/CommonTestAndPreview/index.js +186 -676
  28. package/v2Components/CommonTestAndPreview/messages.js +3 -49
  29. package/v2Components/CommonTestAndPreview/sagas.js +6 -15
  30. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +284 -308
  31. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +65 -231
  32. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +5 -118
  33. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +0 -341
  34. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +1 -8
  35. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +13 -34
  36. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +283 -281
  37. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +1 -199
  38. package/v2Components/CommonTestAndPreview/tests/index.test.js +4 -132
  39. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
  40. package/v2Components/FormBuilder/index.js +10 -8
  41. package/v2Components/TemplatePreview/_templatePreview.scss +23 -33
  42. package/v2Components/TemplatePreview/index.js +28 -143
  43. package/v2Components/TemplatePreview/tests/index.test.js +0 -142
  44. package/v2Components/TestAndPreviewSlidebox/index.js +1 -13
  45. package/v2Components/TestAndPreviewSlidebox/sagas.js +4 -11
  46. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +1 -3
  47. package/v2Containers/Assets/images/archive_Empty_Illustration.svg +9 -0
  48. package/v2Containers/CreativesContainer/SlideBoxContent.js +4 -36
  49. package/v2Containers/CreativesContainer/SlideBoxFooter.js +4 -11
  50. package/v2Containers/CreativesContainer/SlideBoxHeader.js +4 -29
  51. package/v2Containers/CreativesContainer/constants.js +0 -9
  52. package/v2Containers/CreativesContainer/index.js +108 -300
  53. package/v2Containers/CreativesContainer/index.scss +1 -51
  54. package/v2Containers/CreativesContainer/messages.js +4 -0
  55. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +34 -78
  56. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +16 -79
  57. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +0 -8
  58. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +98 -357
  59. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +18 -20
  60. package/v2Containers/CreativesContainer/tests/index.test.js +9 -71
  61. package/v2Containers/Rcs/constants.js +8 -119
  62. package/v2Containers/Rcs/index.js +812 -2375
  63. package/v2Containers/Rcs/index.scss +6 -276
  64. package/v2Containers/Rcs/messages.js +3 -38
  65. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +70345 -98302
  66. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +5 -0
  67. package/v2Containers/Rcs/tests/index.test.js +121 -152
  68. package/v2Containers/Rcs/tests/mockData.js +0 -38
  69. package/v2Containers/Rcs/tests/utils.test.js +30 -646
  70. package/v2Containers/Rcs/utils.js +11 -478
  71. package/v2Containers/Sms/Create/index.js +40 -100
  72. package/v2Containers/SmsTrai/Create/index.js +4 -9
  73. package/v2Containers/SmsTrai/Edit/constants.js +0 -2
  74. package/v2Containers/SmsTrai/Edit/index.js +130 -636
  75. package/v2Containers/SmsTrai/Edit/messages.js +4 -14
  76. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +2296 -4249
  77. package/v2Containers/SmsWrapper/index.js +8 -37
  78. package/v2Containers/TagList/index.js +0 -6
  79. package/v2Containers/Templates/ChannelTypeIllustration.js +23 -6
  80. package/v2Containers/Templates/_templates.scss +126 -181
  81. package/v2Containers/Templates/actions.js +36 -11
  82. package/v2Containers/Templates/constants.js +23 -2
  83. package/v2Containers/Templates/index.js +333 -142
  84. package/v2Containers/Templates/messages.js +68 -0
  85. package/v2Containers/Templates/reducer.js +68 -0
  86. package/v2Containers/Templates/sagas.js +98 -55
  87. package/v2Containers/Templates/selectors.js +12 -0
  88. package/v2Containers/Templates/tests/ChannelTypeIllustration.test.js +12 -0
  89. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1256 -1042
  90. package/v2Containers/Templates/tests/index.test.js +6 -0
  91. package/v2Containers/Templates/tests/reducer.test.js +178 -0
  92. package/v2Containers/Templates/tests/sagas.test.js +436 -200
  93. package/v2Containers/Templates/tests/selector.test.js +32 -0
  94. package/v2Containers/TemplatesV2/TemplatesV2.style.js +1 -72
  95. package/v2Containers/TemplatesV2/index.js +23 -86
  96. package/v2Containers/Whatsapp/index.js +20 -3
  97. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +34 -578
  98. package/utils/rcsPayloadUtils.js +0 -92
  99. package/utils/templateVarUtils.js +0 -201
  100. package/utils/tests/templateVarUtils.test.js +0 -204
  101. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js.rej +0 -18
  102. package/v2Components/CommonTestAndPreview/previewApiUtils.js +0 -59
  103. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +0 -67
  104. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +0 -87
  105. package/v2Components/SmsFallback/constants.js +0 -73
  106. package/v2Components/SmsFallback/index.js +0 -955
  107. package/v2Components/SmsFallback/index.scss +0 -265
  108. package/v2Components/SmsFallback/messages.js +0 -78
  109. package/v2Components/SmsFallback/smsFallbackUtils.js +0 -118
  110. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +0 -50
  111. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +0 -147
  112. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +0 -304
  113. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +0 -197
  114. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +0 -277
  115. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +0 -422
  116. package/v2Components/SmsFallback/useLocalTemplateList.js +0 -92
  117. package/v2Components/TemplatePreview/constants.js +0 -2
  118. package/v2Components/VarSegmentMessageEditor/constants.js +0 -2
  119. package/v2Components/VarSegmentMessageEditor/index.js +0 -125
  120. package/v2Components/VarSegmentMessageEditor/index.scss +0 -46
  121. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +0 -43
  122. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +0 -67
  123. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +0 -90
  124. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +0 -258
  125. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +0 -125
  126. package/v2Containers/Rcs/index.js.rej +0 -1336
  127. package/v2Containers/Rcs/index.scss.rej +0 -74
  128. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +0 -225
  129. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap.rej +0 -128
  130. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +0 -318
  131. package/v2Containers/Sms/smsFormDataHelpers.js +0 -67
  132. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +0 -253
  133. package/v2Containers/SmsTrai/Edit/index.scss +0 -121
  134. package/v2Containers/Templates/TemplatesActionBar.js +0 -101
  135. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +0 -120
  136. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +0 -180
  137. package/v2Containers/Templates/utils/smsTemplatesListApi.js +0 -79
  138. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +0 -131
@@ -1,95 +1,83 @@
1
1
  /**
2
+ * Tests for CustomValuesEditor component.
3
+ * Covers: loading state, values-missing warning, JSON view, tag table, discard/update buttons.
2
4
  * @jest-environment jsdom
3
5
  */
6
+
4
7
  import React from 'react';
5
8
  import { render, screen, fireEvent } from '@testing-library/react';
6
- import '@testing-library/jest-dom';
7
9
  import { IntlProvider } from 'react-intl';
8
10
  import PropTypes from 'prop-types';
9
- import CustomValuesEditor from '../CustomValuesEditor';
10
11
 
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
- });
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
+ ));
63
53
 
64
- const formatMessage = (msg) => (typeof msg === 'object' && msg?.id ? msg.id : String(msg));
54
+ import CustomValuesEditor from '../CustomValuesEditor';
65
55
 
66
- const baseProps = {
56
+ const defaultProps = {
67
57
  isExtractingTags: false,
68
58
  isUpdatePreviewDisabled: false,
69
59
  showJSON: false,
70
60
  setShowJSON: jest.fn(),
71
- customValues: { 'tag.a': 'v1' },
61
+ customValues: { '{{customer.name}}': 'Alice', '{{customer.email}}': '' },
72
62
  handleJSONTextChange: jest.fn(),
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
- },
63
+ extractedTags: [
64
+ { fullPath: '{{customer.name}}' },
65
+ { fullPath: '{{customer.email}}' },
80
66
  ],
67
+ requiredTags: [{ fullPath: '{{customer.name}}' }],
68
+ optionalTags: [{ fullPath: '{{customer.email}}' }],
81
69
  handleCustomValueChange: jest.fn(),
82
70
  handleDiscardCustomValues: jest.fn(),
83
71
  handleUpdatePreview: jest.fn(),
84
72
  isUpdatingPreview: false,
85
- formatMessage,
73
+ formatMessage: jest.fn((msg) => msg.defaultMessage || msg.id),
86
74
  };
87
75
 
88
76
  // IntlProvider wrapper for incoming tests
89
77
  const scope = 'app.v2Components.TestAndPreviewSlidebox';
90
78
  const mockMessages = {
91
79
  [`${scope}.extractingTags`]: 'Extracting tags...',
92
- [`${scope}.valuesMissing`]: 'Some values are missing',
80
+ [`${scope}.valuesMissing`]: 'Values missing for some of the personalization tags',
93
81
  [`${scope}.showJSON`]: 'Show JSON',
94
82
  [`${scope}.personalizationTags`]: 'Personalization Tags',
95
83
  [`${scope}.customValues`]: 'Custom Values',
@@ -109,268 +97,256 @@ TestWrapper.propTypes = {
109
97
  };
110
98
 
111
99
  describe('CustomValuesEditor', () => {
112
- beforeEach(() => {
113
- jest.clearAllMocks();
114
- });
100
+ beforeEach(() => jest.clearAllMocks());
115
101
 
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();
123
- });
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
+ });
124
112
 
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
- });
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
+ });
133
122
 
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();
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
+ });
143
132
  });
144
133
 
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
- });
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
+ });
156
143
 
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');
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
+ });
167
154
  });
168
155
 
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
- });
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
+ });
181
168
 
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
- });
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
+ });
196
180
 
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
- });
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
+ });
215
191
 
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();
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
+ });
233
200
  });
234
201
 
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
- });
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
+ });
254
212
 
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();
272
- });
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
+ });
273
222
 
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
- });
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
+ });
293
231
 
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
- });
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
+ });
314
246
 
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);
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
+ });
257
+
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
+ });
334
271
  });
335
272
 
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);
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
+ });
283
+
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
+ });
350
293
  });
351
294
 
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');
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
+ });
306
+
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
+ });
317
+
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
+ });
327
+
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
+ });
337
+
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
+ });
375
351
  });
376
352
  });