@capillarytech/creatives-library 9.0.15-alpha.3 → 9.0.16-beta.0

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 (146) hide show
  1. package/AppRoot.js +80 -0
  2. package/app.js +5 -3
  3. package/constants/unified.js +0 -29
  4. package/entry.js +67 -1
  5. package/global-styles.js +1 -1
  6. package/mfe-exposed-components.js +2 -4
  7. package/package.json +2 -2
  8. package/services/api.js +1 -1
  9. package/services/tests/api.test.js +20 -35
  10. package/styles/containers/layout/_layoutPage.scss +8 -6
  11. package/utils/commonUtils.js +1 -19
  12. package/utils/getDataLayer.js +15 -0
  13. package/utils/gtmTrackers/gtmEvents/creativeDetails.js +2 -1
  14. package/utils/mfeDetect.js +1 -0
  15. package/v2Components/CapActionButton/constants.js +1 -8
  16. package/v2Components/CapActionButton/index.js +144 -202
  17. package/v2Components/CapActionButton/index.scss +14 -232
  18. package/v2Components/CapActionButton/messages.js +3 -19
  19. package/v2Components/CapActionButton/tests/index.test.js +18 -42
  20. package/v2Components/CapImageUpload/index.js +2 -2
  21. package/v2Components/CapTagList/index.js +0 -10
  22. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +48 -70
  23. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +2 -8
  24. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +21 -214
  25. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +0 -16
  26. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +9 -83
  27. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +0 -30
  28. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +11 -60
  29. package/v2Components/CommonTestAndPreview/SendTestMessage.js +5 -10
  30. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +22 -202
  31. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +80 -381
  32. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +8 -155
  33. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +0 -11
  34. package/v2Components/CommonTestAndPreview/constants.js +2 -38
  35. package/v2Components/CommonTestAndPreview/index.js +222 -761
  36. package/v2Components/CommonTestAndPreview/messages.js +3 -45
  37. package/v2Components/CommonTestAndPreview/sagas.js +6 -25
  38. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +0 -1
  39. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +267 -284
  40. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +65 -231
  41. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +5 -118
  42. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +0 -341
  43. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +1 -8
  44. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +13 -34
  45. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +283 -281
  46. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +1 -199
  47. package/v2Components/CommonTestAndPreview/tests/index.test.js +4 -133
  48. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +24 -31
  49. package/v2Components/NavigationBar/index.js +9 -7
  50. package/v2Components/NavigationBar/mfeModuleHeader.config.js +16 -0
  51. package/v2Components/Pagination/_pagination.scss +2 -0
  52. package/v2Components/TemplatePreview/_templatePreview.scss +25 -45
  53. package/v2Components/TemplatePreview/index.js +32 -147
  54. package/v2Components/TemplatePreview/tests/index.test.js +0 -142
  55. package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +6 -6
  56. package/v2Components/TestAndPreviewSlidebox/index.js +1 -13
  57. package/v2Components/TestAndPreviewSlidebox/sagas.js +4 -11
  58. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +1 -3
  59. package/v2Containers/Cap/constants.js +1 -0
  60. package/v2Containers/Cap/index.js +47 -21
  61. package/v2Containers/Cap/sagas.js +19 -0
  62. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +1 -4
  63. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +0 -4
  64. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +20 -20
  65. package/v2Containers/CreativesContainer/SlideBoxContent.js +7 -37
  66. package/v2Containers/CreativesContainer/SlideBoxFooter.js +5 -14
  67. package/v2Containers/CreativesContainer/SlideBoxHeader.js +5 -36
  68. package/v2Containers/CreativesContainer/constants.js +0 -11
  69. package/v2Containers/CreativesContainer/index.js +104 -323
  70. package/v2Containers/CreativesContainer/index.scss +1 -83
  71. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +34 -79
  72. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +16 -79
  73. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +0 -8
  74. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +90 -333
  75. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +15 -20
  76. package/v2Containers/CreativesContainer/tests/index.test.js +9 -71
  77. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  78. package/v2Containers/Rcs/constants.js +11 -131
  79. package/v2Containers/Rcs/index.js +829 -2682
  80. package/v2Containers/Rcs/index.scss +8 -443
  81. package/v2Containers/Rcs/messages.js +6 -45
  82. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +39409 -74176
  83. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +74 -10
  84. package/v2Containers/Rcs/tests/index.test.js +40 -151
  85. package/v2Containers/Rcs/tests/mockData.js +0 -38
  86. package/v2Containers/Rcs/tests/utils.test.js +30 -643
  87. package/v2Containers/Rcs/utils.js +12 -466
  88. package/v2Containers/Sms/Create/index.js +48 -115
  89. package/v2Containers/SmsTrai/Create/index.js +4 -9
  90. package/v2Containers/SmsTrai/Edit/constants.js +0 -2
  91. package/v2Containers/SmsTrai/Edit/index.js +169 -678
  92. package/v2Containers/SmsTrai/Edit/messages.js +4 -14
  93. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +2854 -5455
  94. package/v2Containers/SmsWrapper/index.js +8 -37
  95. package/v2Containers/TagList/index.js +0 -6
  96. package/v2Containers/Templates/_templates.scss +11 -196
  97. package/v2Containers/Templates/actions.js +0 -11
  98. package/v2Containers/Templates/constants.js +0 -2
  99. package/v2Containers/Templates/index.js +60 -131
  100. package/v2Containers/Templates/sagas.js +13 -57
  101. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1015 -1060
  102. package/v2Containers/Templates/tests/sagas.test.js +16 -199
  103. package/v2Containers/TemplatesV2/TemplatesV2.style.js +10 -75
  104. package/v2Containers/TemplatesV2/index.js +43 -86
  105. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  106. package/v2Containers/Whatsapp/index.js +25 -4
  107. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +34 -578
  108. package/utils/rcsPayloadUtils.js +0 -92
  109. package/utils/templateVarUtils.js +0 -201
  110. package/utils/tests/rcsPayloadUtils.test.js +0 -226
  111. package/utils/tests/templateVarUtils.test.js +0 -204
  112. package/v2Components/CommonTestAndPreview/previewApiUtils.js +0 -59
  113. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +0 -67
  114. package/v2Components/CommonTestAndPreview/utils.js +0 -84
  115. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +0 -91
  116. package/v2Components/SmsFallback/constants.js +0 -94
  117. package/v2Components/SmsFallback/index.js +0 -958
  118. package/v2Components/SmsFallback/index.scss +0 -266
  119. package/v2Components/SmsFallback/messages.js +0 -78
  120. package/v2Components/SmsFallback/smsFallbackUtils.js +0 -120
  121. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +0 -50
  122. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +0 -147
  123. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +0 -304
  124. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +0 -208
  125. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +0 -309
  126. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +0 -422
  127. package/v2Components/SmsFallback/useLocalTemplateList.js +0 -92
  128. package/v2Components/TemplatePreview/constants.js +0 -2
  129. package/v2Components/VarSegmentMessageEditor/constants.js +0 -2
  130. package/v2Components/VarSegmentMessageEditor/index.js +0 -125
  131. package/v2Components/VarSegmentMessageEditor/index.scss +0 -46
  132. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +0 -17
  133. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +0 -79
  134. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +0 -90
  135. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +0 -258
  136. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +0 -125
  137. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +0 -227
  138. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +0 -318
  139. package/v2Containers/Sms/smsFormDataHelpers.js +0 -67
  140. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +0 -253
  141. package/v2Containers/SmsTrai/Edit/index.scss +0 -126
  142. package/v2Containers/Templates/TemplatesActionBar.js +0 -101
  143. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +0 -120
  144. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +0 -180
  145. package/v2Containers/Templates/utils/smsTemplatesListApi.js +0 -79
  146. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +0 -131
@@ -1,108 +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,
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
- ],
73
+ formatMessage: jest.fn((msg) => msg.defaultMessage || msg.id),
99
74
  };
100
75
 
101
76
  // IntlProvider wrapper for incoming tests
102
77
  const scope = 'app.v2Components.TestAndPreviewSlidebox';
103
78
  const mockMessages = {
104
79
  [`${scope}.extractingTags`]: 'Extracting tags...',
105
- [`${scope}.valuesMissing`]: 'Some values are missing',
80
+ [`${scope}.valuesMissing`]: 'Values missing for some of the personalization tags',
106
81
  [`${scope}.showJSON`]: 'Show JSON',
107
82
  [`${scope}.personalizationTags`]: 'Personalization Tags',
108
83
  [`${scope}.customValues`]: 'Custom Values',
@@ -122,129 +97,119 @@ TestWrapper.propTypes = {
122
97
  };
123
98
 
124
99
  describe('CustomValuesEditor', () => {
125
- beforeEach(() => {
126
- jest.clearAllMocks();
127
- });
100
+ beforeEach(() => jest.clearAllMocks());
128
101
 
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
- });
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
+ });
137
112
 
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();
145
- });
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
+ });
146
122
 
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();
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
+ });
156
132
  });
157
133
 
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('{}');
168
- });
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
+ });
169
143
 
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');
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
+ });
180
154
  });
181
155
 
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
- });
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
+ });
194
168
 
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
- });
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
+ });
209
180
 
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();
227
- });
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
+ });
228
191
 
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();
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
+ });
246
200
  });
247
201
 
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
+ });
212
+
248
213
  it('renders required tags with a required indicator (*)', () => {
249
214
  render(
250
215
  <TestWrapper>
@@ -274,106 +239,124 @@ describe('CustomValuesEditor', () => {
274
239
  expect(screen.getByText('{{customer.email}}')).toBeTruthy();
275
240
  });
276
241
 
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);
295
- });
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
+ });
296
256
 
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);
316
- });
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
+ });
317
267
 
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);
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
+ });
337
281
  });
338
282
 
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);
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
+ });
353
303
  });
354
304
 
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');
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
+ });
337
+
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
+ });
347
+
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
+ });
378
361
  });
379
362
  });