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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/constants/unified.js +29 -0
  2. package/global-styles.js +1 -1
  3. package/package.json +1 -1
  4. package/services/tests/api.test.js +35 -20
  5. package/utils/commonUtils.js +22 -1
  6. package/utils/rcsPayloadUtils.js +102 -0
  7. package/utils/templateVarUtils.js +198 -0
  8. package/utils/tests/rcsPayloadUtils.test.js +295 -0
  9. package/utils/tests/templateVarUtils.test.js +204 -0
  10. package/v2Components/CapActionButton/constants.js +21 -1
  11. package/v2Components/CapActionButton/index.js +260 -144
  12. package/v2Components/CapActionButton/index.scss +245 -14
  13. package/v2Components/CapActionButton/messages.js +32 -3
  14. package/v2Components/CapActionButton/tests/index.test.js +74 -19
  15. package/v2Components/CapImageUpload/index.js +2 -2
  16. package/v2Components/CapTagList/index.js +9 -0
  17. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -48
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  23. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +58 -11
  24. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +186 -22
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +381 -80
  27. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +155 -8
  28. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  29. package/v2Components/CommonTestAndPreview/constants.js +35 -2
  30. package/v2Components/CommonTestAndPreview/index.js +770 -231
  31. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  32. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  33. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  34. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +1 -0
  35. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +412 -257
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +337 -63
  37. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  38. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  39. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  40. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +108 -15
  41. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  42. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  43. package/v2Components/CommonTestAndPreview/tests/index.test.js +133 -4
  44. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  45. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
  46. package/v2Components/CommonTestAndPreview/tests/utils.test.js +151 -0
  47. package/v2Components/CommonTestAndPreview/utils.js +84 -0
  48. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  49. package/v2Components/SmsFallback/constants.js +94 -0
  50. package/v2Components/SmsFallback/index.js +958 -0
  51. package/v2Components/SmsFallback/index.scss +266 -0
  52. package/v2Components/SmsFallback/messages.js +78 -0
  53. package/v2Components/SmsFallback/smsFallbackUtils.js +120 -0
  54. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  55. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  56. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  57. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +208 -0
  58. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  59. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +471 -0
  60. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  61. package/v2Components/TemplatePreview/_templatePreview.scss +45 -25
  62. package/v2Components/TemplatePreview/constants.js +2 -0
  63. package/v2Components/TemplatePreview/index.js +147 -32
  64. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  65. package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +6 -6
  66. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  67. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  68. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  69. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  70. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  71. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  72. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +4 -1
  73. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +4 -0
  74. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +20 -20
  75. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +27 -0
  76. package/v2Containers/CreativesContainer/SlideBoxContent.js +37 -4
  77. package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
  78. package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
  79. package/v2Containers/CreativesContainer/constants.js +11 -0
  80. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +77 -0
  81. package/v2Containers/CreativesContainer/index.js +328 -106
  82. package/v2Containers/CreativesContainer/index.scss +102 -1
  83. package/v2Containers/CreativesContainer/tests/CreativesSlideBoxWrapper.test.js +58 -0
  84. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  85. package/v2Containers/CreativesContainer/tests/SlideBoxContent.smsDltPreview.test.js +73 -0
  86. package/v2Containers/CreativesContainer/tests/SlideBoxContent.test.js +37 -0
  87. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +103 -34
  88. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +193 -15
  89. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +88 -0
  90. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +549 -57
  91. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  92. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  93. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  94. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  95. package/v2Containers/Email/index.js +3 -63
  96. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +4 -9
  97. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  98. package/v2Containers/Rcs/carouselUtils.js +224 -0
  99. package/v2Containers/Rcs/components/CarouselCard.js +317 -0
  100. package/v2Containers/Rcs/components/CarouselCardButtons.js +113 -0
  101. package/v2Containers/Rcs/components/CarouselCardMedia.js +136 -0
  102. package/v2Containers/Rcs/components/CarouselCharacterCount.js +31 -0
  103. package/v2Containers/Rcs/components/CarouselDimensionSelection.js +80 -0
  104. package/v2Containers/Rcs/constants.js +132 -16
  105. package/v2Containers/Rcs/index.js +1808 -949
  106. package/v2Containers/Rcs/index.scss +443 -8
  107. package/v2Containers/Rcs/messages.js +45 -22
  108. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +484 -0
  109. package/v2Containers/Rcs/tests/CarouselCard.test.js +464 -0
  110. package/v2Containers/Rcs/tests/CarouselCardButtons.test.js +211 -0
  111. package/v2Containers/Rcs/tests/CarouselCardMedia.test.js +160 -0
  112. package/v2Containers/Rcs/tests/CarouselCharacterCount.test.js +50 -0
  113. package/v2Containers/Rcs/tests/CarouselDimensionSelection.test.js +119 -0
  114. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +74176 -39409
  115. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +10 -74
  116. package/v2Containers/Rcs/tests/carouselUtils.test.js +916 -0
  117. package/v2Containers/Rcs/tests/index.test.js +219 -40
  118. package/v2Containers/Rcs/tests/mockData.js +38 -0
  119. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +873 -0
  120. package/v2Containers/Rcs/tests/utils.test.js +682 -30
  121. package/v2Containers/Rcs/utils.js +514 -12
  122. package/v2Containers/Sms/Create/index.js +115 -48
  123. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  124. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  125. package/v2Containers/SmsTrai/Create/index.js +9 -4
  126. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  127. package/v2Containers/SmsTrai/Edit/index.js +678 -169
  128. package/v2Containers/SmsTrai/Edit/index.scss +126 -0
  129. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  130. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
  131. package/v2Containers/SmsWrapper/index.js +37 -8
  132. package/v2Containers/TagList/index.js +5 -0
  133. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  134. package/v2Containers/Templates/_templates.scss +196 -12
  135. package/v2Containers/Templates/actions.js +11 -0
  136. package/v2Containers/Templates/constants.js +2 -0
  137. package/v2Containers/Templates/index.js +131 -59
  138. package/v2Containers/Templates/sagas.js +57 -13
  139. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +129 -0
  140. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
  141. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  142. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  143. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  144. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  145. package/v2Containers/TemplatesV2/index.js +88 -25
  146. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  147. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  148. package/v2Containers/Whatsapp/index.js +3 -20
  149. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -0,0 +1,464 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ import React from 'react';
5
+ import { render, screen, fireEvent } from '@testing-library/react';
6
+ import '@testing-library/jest-dom';
7
+ import CarouselCard from '../components/CarouselCard';
8
+
9
+ jest.mock('@capillarytech/cap-ui-library/CapCard', () => {
10
+ const React = require('react');
11
+ return function MockCapCard(props) {
12
+ return React.createElement(
13
+ 'div',
14
+ { 'data-testid': 'cap-card', className: props?.className },
15
+ React.createElement('div', { 'data-testid': 'cap-card-title' }, props?.title),
16
+ React.createElement('div', { 'data-testid': 'cap-card-extra' }, props?.extra),
17
+ props?.children,
18
+ );
19
+ };
20
+ });
21
+
22
+ jest.mock('@capillarytech/cap-ui-library/CapTooltip', () => {
23
+ const React = require('react');
24
+ return function MockCapTooltip(props) {
25
+ return React.createElement(
26
+ 'div',
27
+ { 'data-testid': 'cap-tooltip', 'data-title': typeof props?.title === 'string' ? props.title : 'complex' },
28
+ props?.children,
29
+ );
30
+ };
31
+ });
32
+
33
+ jest.mock('@capillarytech/cap-ui-library/CapButton', () => {
34
+ const React = require('react');
35
+ return function MockCapButton(props) {
36
+ return React.createElement('button', {
37
+ type: 'button',
38
+ 'data-testid': props?.['data-testid'] || 'cap-button',
39
+ className: props?.className,
40
+ onClick: props?.onClick,
41
+ disabled: props?.disabled,
42
+ 'aria-label': props?.['aria-label'],
43
+ }, props?.children);
44
+ };
45
+ });
46
+
47
+ jest.mock('@capillarytech/cap-ui-library/CapIcon', () => {
48
+ const React = require('react');
49
+ return function MockCapIcon(props) {
50
+ return React.createElement('span', { 'data-testid': 'cap-icon', 'data-type': props?.type });
51
+ };
52
+ });
53
+
54
+ jest.mock('@capillarytech/cap-ui-library/CapRow', () => {
55
+ const React = require('react');
56
+ return function MockCapRow(props) {
57
+ return React.createElement('div', { className: props?.className, 'data-testid': 'cap-row' }, props?.children);
58
+ };
59
+ });
60
+
61
+ jest.mock('@capillarytech/cap-ui-library/CapColumn', () => {
62
+ const React = require('react');
63
+ return function MockCapColumn(props) {
64
+ return React.createElement('div', { className: props?.className, 'data-testid': 'cap-column' }, props?.children);
65
+ };
66
+ });
67
+
68
+ jest.mock('@capillarytech/cap-ui-library/CapHeading', () => {
69
+ const React = require('react');
70
+ return function MockCapHeading(props) {
71
+ return React.createElement(props?.type === 'h5' ? 'h5' : 'h4', { className: props?.className }, props?.children);
72
+ };
73
+ });
74
+
75
+ jest.mock('@capillarytech/cap-ui-library/CapRadioGroup', () => {
76
+ const React = require('react');
77
+ return function MockCapRadioGroup(props) {
78
+ return React.createElement(
79
+ 'div',
80
+ { 'data-testid': 'cap-radio-group', id: props?.id, className: props?.className },
81
+ (props?.options || []).map((option) => React.createElement('label', { key: option.value },
82
+ React.createElement('input', {
83
+ type: 'radio',
84
+ 'data-testid': `radio-${option.value}`,
85
+ checked: props?.value === option.value,
86
+ disabled: props?.disabled,
87
+ onChange: () => props?.onChange?.({ target: { value: option.value } }),
88
+ }),
89
+ option.label)),
90
+ );
91
+ };
92
+ });
93
+
94
+ jest.mock('@capillarytech/cap-ui-library/CapHeader', () => {
95
+ const React = require('react');
96
+ return function MockCapHeader(props) {
97
+ return React.createElement(
98
+ 'div',
99
+ { className: props?.className, 'data-testid': 'cap-header' },
100
+ React.createElement('div', { 'data-testid': 'cap-header-title' }, props?.title),
101
+ React.createElement('div', { 'data-testid': 'cap-header-suffix' }, props?.suffix),
102
+ );
103
+ };
104
+ });
105
+
106
+ jest.mock('@capillarytech/cap-ui-library/CapInput', () => {
107
+ const React = require('react');
108
+ function MockCapInput(props) {
109
+ return React.createElement('input', {
110
+ 'data-testid': 'rcs-title-input',
111
+ value: props?.value,
112
+ placeholder: props?.placeholder,
113
+ onChange: props?.onChange,
114
+ disabled: props?.disabled,
115
+ 'data-error': props?.errorMessage || '',
116
+ });
117
+ }
118
+ MockCapInput.TextArea = function MockTextArea(props) {
119
+ return React.createElement(
120
+ 'div',
121
+ null,
122
+ React.createElement('textarea', {
123
+ 'data-testid': 'rcs-desc-textarea',
124
+ value: props?.value,
125
+ placeholder: props?.placeholder,
126
+ onChange: props?.onChange,
127
+ disabled: props?.disabled,
128
+ }),
129
+ React.createElement('div', { 'data-testid': 'rcs-desc-error' }, props?.errorMessage),
130
+ );
131
+ };
132
+ return MockCapInput;
133
+ });
134
+
135
+ jest.mock('@capillarytech/cap-ui-library/CapError', () => {
136
+ const React = require('react');
137
+ return function MockCapError(props) {
138
+ return React.createElement('div', { className: props?.className, 'data-testid': 'cap-error' }, props?.children);
139
+ };
140
+ });
141
+
142
+ jest.mock('@capillarytech/cap-ui-library/CapDivider', () => {
143
+ const React = require('react');
144
+ return function MockCapDivider(props) {
145
+ return React.createElement('hr', { className: props?.className, 'data-testid': 'cap-divider' });
146
+ };
147
+ });
148
+
149
+ jest.mock('../../TagList', () => {
150
+ const React = require('react');
151
+ return function MockTagList(props) {
152
+ global.__lastTagListProps = props;
153
+ return React.createElement('div', {
154
+ 'data-testid': 'tag-list',
155
+ 'data-label': props?.label,
156
+ 'data-location': JSON.stringify(props?.location || {}),
157
+ 'data-injected-tags': JSON.stringify(props?.injectedTags),
158
+ });
159
+ };
160
+ });
161
+
162
+ jest.mock('../components/CarouselCardMedia', () => {
163
+ const React = require('react');
164
+ return function MockCarouselCardMedia(props) {
165
+ global.__lastMediaProps = props;
166
+ return React.createElement('div', { 'data-testid': 'carousel-card-media' }, 'media');
167
+ };
168
+ });
169
+
170
+ jest.mock('../components/CarouselCardButtons', () => {
171
+ const React = require('react');
172
+ return function MockCarouselCardButtons(props) {
173
+ global.__lastButtonsProps = props;
174
+ return React.createElement('div', { 'data-testid': 'carousel-card-buttons' }, 'buttons');
175
+ };
176
+ });
177
+
178
+ jest.mock('../components/CarouselCharacterCount', () => {
179
+ const React = require('react');
180
+ return function MockCarouselCharacterCount(props) {
181
+ global.__characterCountCalls = global.__characterCountCalls || [];
182
+ global.__characterCountCalls.push(props);
183
+ return React.createElement('div', { 'data-testid': 'character-count' }, `${props?.currentLength}/${props?.maxLength}`);
184
+ };
185
+ });
186
+
187
+ const mediaRadioOptions = [
188
+ { value: 'IMAGE', label: 'Image' },
189
+ { value: 'VIDEO', label: 'Video' },
190
+ ];
191
+
192
+ const baseProps = {
193
+ cardIndex: 0,
194
+ card: { mediaType: 'IMAGE', title: 'Hello', description: 'World' },
195
+ carouselLength: 2,
196
+ isEditFlow: false,
197
+ isFullMode: true,
198
+ mediaRadioOptions,
199
+ cardError: {},
200
+ location: { pathname: '/rcs' },
201
+ tagsForPicker: [],
202
+ injectedTags: {},
203
+ selectedOfferDetails: [],
204
+ // Plain function (not jest.fn) — this repo's jest config sets `resetMocks: true`,
205
+ // which strips a jest.fn's implementation before every test, so a shared
206
+ // module-level jest.fn(impl) would silently return undefined after the first test.
207
+ formatMessage: (msg) => msg?.defaultMessage || msg?.id || String(msg),
208
+ titleMaxLength: 100,
209
+ descriptionMaxLength: 500,
210
+ titleCharCount: 5,
211
+ descriptionCharCount: 5,
212
+ dimKey: 'SHORT_SMALL',
213
+ rcsData: {},
214
+ uploadRcsImage: jest.fn(),
215
+ uploadRcsVideo: jest.fn(),
216
+ hostName: 'host-1',
217
+ onDelete: jest.fn(),
218
+ onMediaTypeChange: jest.fn(),
219
+ onTitleChange: jest.fn(),
220
+ onDescriptionChange: jest.fn(),
221
+ onTitleAddVar: jest.fn(),
222
+ onDescriptionAddVar: jest.fn(),
223
+ onTagSelect: jest.fn(),
224
+ onContextChange: jest.fn(),
225
+ renderEditMessage: (val) => <div data-testid="render-edit-message">{val}</div>,
226
+ onFieldChange: jest.fn(),
227
+ onSuggestionsChange: jest.fn(),
228
+ };
229
+
230
+ describe('CarouselCard', () => {
231
+ beforeEach(() => {
232
+ jest.clearAllMocks();
233
+ global.__lastMediaProps = undefined;
234
+ global.__lastButtonsProps = undefined;
235
+ global.__lastTagListProps = undefined;
236
+ global.__characterCountCalls = [];
237
+ });
238
+
239
+ it('renders the card title based on cardIndex', () => {
240
+ render(<CarouselCard {...baseProps} cardIndex={3} />);
241
+ expect(screen.getByTestId('cap-card-title').textContent).toBe('Card 4');
242
+ });
243
+
244
+ describe('delete button (extra) — isEditFlow branch', () => {
245
+ it('renders no extra content when isEditFlow is true', () => {
246
+ render(<CarouselCard {...baseProps} isEditFlow />);
247
+ expect(screen.getByTestId('cap-card-extra').textContent).toBe('');
248
+ });
249
+
250
+ it('renders a disabled delete button wrapped in a tooltip when carouselLength === 1', () => {
251
+ render(<CarouselCard {...baseProps} isEditFlow={false} carouselLength={1} />);
252
+ expect(screen.getByTestId('cap-tooltip')).toBeInTheDocument();
253
+ const deleteBtn = screen.getByRole('button', { name: /At least 1 card/i });
254
+ expect(deleteBtn).toBeDisabled();
255
+ fireEvent.click(deleteBtn);
256
+ expect(baseProps.onDelete).not.toHaveBeenCalled();
257
+ });
258
+
259
+ it('renders an enabled delete button (no tooltip) when carouselLength > 1, and calls onDelete on click', () => {
260
+ render(<CarouselCard {...baseProps} isEditFlow={false} carouselLength={2} />);
261
+ expect(screen.queryByTestId('cap-tooltip')).not.toBeInTheDocument();
262
+ const deleteBtn = screen.getByRole('button', { name: /delete/i });
263
+ expect(deleteBtn).not.toBeDisabled();
264
+ fireEvent.click(deleteBtn);
265
+ expect(baseProps.onDelete).toHaveBeenCalled();
266
+ });
267
+ });
268
+
269
+ describe('media type selection — isEditFlow branch', () => {
270
+ it('shows a read-only media type label matching mediaRadioOptions when isEditFlow is true', () => {
271
+ render(<CarouselCard {...baseProps} isEditFlow card={{ mediaType: 'VIDEO' }} />);
272
+ expect(screen.getByText('Video')).toBeInTheDocument();
273
+ expect(screen.queryByTestId('cap-radio-group')).not.toBeInTheDocument();
274
+ });
275
+
276
+ it('shows "-" fallback in read-only media label when no option matches', () => {
277
+ render(<CarouselCard {...baseProps} isEditFlow card={{ mediaType: 'UNKNOWN' }} />);
278
+ expect(screen.getByText('-')).toBeInTheDocument();
279
+ });
280
+
281
+ it('shows a CapRadioGroup when isEditFlow is false, disabled by !isFullMode', () => {
282
+ render(<CarouselCard {...baseProps} isEditFlow={false} isFullMode={false} />);
283
+ const radioGroup = screen.getByTestId('cap-radio-group');
284
+ expect(radioGroup).toBeInTheDocument();
285
+ expect(screen.getByTestId('radio-IMAGE')).toBeDisabled();
286
+ });
287
+
288
+ it('enables the CapRadioGroup when isFullMode is true and fires onMediaTypeChange on change', () => {
289
+ render(<CarouselCard {...baseProps} isEditFlow={false} isFullMode />);
290
+ expect(screen.getByTestId('radio-IMAGE')).not.toBeDisabled();
291
+ fireEvent.click(screen.getByTestId('radio-VIDEO'));
292
+ expect(baseProps.onMediaTypeChange).toHaveBeenCalledWith('VIDEO');
293
+ });
294
+ });
295
+
296
+ it('forwards media props to CarouselCardMedia', () => {
297
+ render(<CarouselCard {...baseProps} cardIndex={2} dimKey="MEDIUM_MEDIUM" />);
298
+ expect(screen.getByTestId('carousel-card-media')).toBeInTheDocument();
299
+ expect(global.__lastMediaProps.cardIndex).toBe(2);
300
+ expect(global.__lastMediaProps.dimKey).toBe('MEDIUM_MEDIUM');
301
+ expect(global.__lastMediaProps.card).toEqual(baseProps.card);
302
+ });
303
+
304
+ describe('title row — (isEditFlow || !isFullMode) branch', () => {
305
+ it('renders TagList suffix and renderEditMessage content when isEditFlow is true', () => {
306
+ render(<CarouselCard {...baseProps} isEditFlow />);
307
+ const tagLists = screen.getAllByTestId('tag-list');
308
+ expect(tagLists.length).toBeGreaterThan(0);
309
+ const editMessages = screen.getAllByTestId('render-edit-message');
310
+ expect(editMessages[0].textContent).toBe('Hello');
311
+ });
312
+
313
+ it('renders TagList suffix and renderEditMessage content when isFullMode is false (even if isEditFlow is false)', () => {
314
+ render(<CarouselCard {...baseProps} isEditFlow={false} isFullMode={false} />);
315
+ expect(screen.getAllByTestId('tag-list').length).toBeGreaterThan(0);
316
+ expect(screen.getAllByTestId('render-edit-message')[0].textContent).toBe('Hello');
317
+ expect(screen.queryByTestId('rcs-title-input')).not.toBeInTheDocument();
318
+ });
319
+
320
+ it('renders add-var CapButton and CapInput when isEditFlow is false and isFullMode is true', () => {
321
+ render(<CarouselCard {...baseProps} isEditFlow={false} isFullMode />);
322
+ expect(screen.queryByTestId('tag-list')).not.toBeInTheDocument();
323
+ const addVarBtn = screen.getByTestId('rcs-carousel-title-add-var-0');
324
+ expect(addVarBtn).toBeInTheDocument();
325
+ expect(screen.getByTestId('rcs-title-input')).toBeInTheDocument();
326
+ });
327
+
328
+ it('calls onTitleAddVar when add-var button is clicked, and it is enabled when cardError.title is absent', () => {
329
+ render(<CarouselCard {...baseProps} isEditFlow={false} isFullMode cardError={{}} />);
330
+ const addVarBtn = screen.getByTestId('rcs-carousel-title-add-var-0');
331
+ expect(addVarBtn).not.toBeDisabled();
332
+ fireEvent.click(addVarBtn);
333
+ expect(baseProps.onTitleAddVar).toHaveBeenCalled();
334
+ });
335
+
336
+ it('disables the title add-var button when cardError.title is present', () => {
337
+ render(<CarouselCard {...baseProps} isEditFlow={false} isFullMode cardError={{ title: 'Too long' }} />);
338
+ expect(screen.getByTestId('rcs-carousel-title-add-var-0')).toBeDisabled();
339
+ });
340
+
341
+ it('passes cardError.title as errorMessage and fires onTitleChange on the CapInput', () => {
342
+ render(<CarouselCard {...baseProps} isEditFlow={false} isFullMode cardError={{ title: 'Bad title' }} />);
343
+ const input = screen.getByTestId('rcs-title-input');
344
+ expect(input.getAttribute('data-error')).toBe('Bad title');
345
+ fireEvent.change(input, { target: { value: 'New title' } });
346
+ expect(baseProps.onTitleChange).toHaveBeenCalled();
347
+ });
348
+
349
+ it('defaults card.title to empty string when absent', () => {
350
+ render(<CarouselCard {...baseProps} isEditFlow={false} isFullMode card={{ mediaType: 'IMAGE' }} />);
351
+ expect(screen.getByTestId('rcs-title-input').value).toBe('');
352
+ });
353
+ });
354
+
355
+ describe('title character count row — !isEditFlow branch', () => {
356
+ it('renders CarouselCharacterCount for title when isEditFlow is false', () => {
357
+ render(<CarouselCard {...baseProps} isEditFlow={false} titleCharCount={12} titleMaxLength={50} />);
358
+ const call = global.__characterCountCalls.find((c) => c.currentLength === 12 && c.maxLength === 50);
359
+ expect(call).toBeTruthy();
360
+ });
361
+
362
+ it('does not render the title CarouselCharacterCount when isEditFlow is true', () => {
363
+ render(<CarouselCard {...baseProps} isEditFlow />);
364
+ // Only description would also be hidden; assert no character-count elements rendered at all
365
+ expect(screen.queryByTestId('character-count')).not.toBeInTheDocument();
366
+ });
367
+ });
368
+
369
+ describe('description row — (isEditFlow || !isFullMode) branch', () => {
370
+ it('renders TagList suffix and renderEditMessage content for description when isEditFlow is true', () => {
371
+ render(<CarouselCard {...baseProps} isEditFlow />);
372
+ const editMessages = screen.getAllByTestId('render-edit-message');
373
+ expect(editMessages[1].textContent).toBe('World');
374
+ });
375
+
376
+ it('renders add-var CapButton and TextArea for description when isEditFlow is false and isFullMode is true', () => {
377
+ render(<CarouselCard {...baseProps} isEditFlow={false} isFullMode />);
378
+ const addVarBtn = screen.getByTestId('rcs-carousel-desc-add-var-0');
379
+ expect(addVarBtn).toBeInTheDocument();
380
+ expect(screen.getByTestId('rcs-desc-textarea')).toBeInTheDocument();
381
+ });
382
+
383
+ it('calls onDescriptionAddVar and enables the button when cardError.description is absent', () => {
384
+ render(<CarouselCard {...baseProps} isEditFlow={false} isFullMode cardError={{}} />);
385
+ const addVarBtn = screen.getByTestId('rcs-carousel-desc-add-var-0');
386
+ expect(addVarBtn).not.toBeDisabled();
387
+ fireEvent.click(addVarBtn);
388
+ expect(baseProps.onDescriptionAddVar).toHaveBeenCalled();
389
+ });
390
+
391
+ it('disables the description add-var button when cardError.description is present', () => {
392
+ render(<CarouselCard {...baseProps} isEditFlow={false} isFullMode cardError={{ description: 'Too long' }} />);
393
+ expect(screen.getByTestId('rcs-carousel-desc-add-var-0')).toBeDisabled();
394
+ });
395
+
396
+ it('renders a CapError with cardError.description when present, and fires onDescriptionChange', () => {
397
+ render(<CarouselCard {...baseProps} isEditFlow={false} isFullMode cardError={{ description: 'Desc too long' }} />);
398
+ expect(screen.getByTestId('cap-error').textContent).toBe('Desc too long');
399
+ const textarea = screen.getByTestId('rcs-desc-textarea');
400
+ fireEvent.change(textarea, { target: { value: 'New desc' } });
401
+ expect(baseProps.onDescriptionChange).toHaveBeenCalled();
402
+ });
403
+
404
+ it('does not render a CapError when cardError.description is absent', () => {
405
+ render(<CarouselCard {...baseProps} isEditFlow={false} isFullMode cardError={{}} />);
406
+ expect(screen.queryByTestId('cap-error')).not.toBeInTheDocument();
407
+ });
408
+
409
+ it('defaults card.description to empty string when absent', () => {
410
+ render(<CarouselCard {...baseProps} isEditFlow={false} isFullMode card={{ mediaType: 'IMAGE' }} />);
411
+ expect(screen.getByTestId('rcs-desc-textarea').value).toBe('');
412
+ });
413
+ });
414
+
415
+ describe('description character count row — !isEditFlow branch', () => {
416
+ it('renders CarouselCharacterCount for description when isEditFlow is false', () => {
417
+ render(<CarouselCard {...baseProps} isEditFlow={false} descriptionCharCount={22} descriptionMaxLength={500} />);
418
+ const call = global.__characterCountCalls.find((c) => c.currentLength === 22 && c.maxLength === 500);
419
+ expect(call).toBeTruthy();
420
+ });
421
+
422
+ it('renders both title and description character counts when isEditFlow is false', () => {
423
+ render(<CarouselCard {...baseProps} isEditFlow={false} />);
424
+ expect(screen.getAllByTestId('character-count')).toHaveLength(2);
425
+ });
426
+ });
427
+
428
+ it('renders a CapDivider before the buttons section', () => {
429
+ render(<CarouselCard {...baseProps} />);
430
+ expect(screen.getByTestId('cap-divider')).toBeInTheDocument();
431
+ });
432
+
433
+ it('forwards suggestions and related props to CarouselCardButtons', () => {
434
+ render(
435
+ <CarouselCard
436
+ {...baseProps}
437
+ cardIndex={1}
438
+ card={{ ...baseProps.card, suggestions: [{ index: 0, text: 'A' }] }}
439
+ />,
440
+ );
441
+ expect(screen.getByTestId('carousel-card-buttons')).toBeInTheDocument();
442
+ expect(global.__lastButtonsProps.cardIndex).toBe(1);
443
+ expect(global.__lastButtonsProps.suggestions).toEqual([{ index: 0, text: 'A' }]);
444
+ expect(global.__lastButtonsProps.hostName).toBe('host-1');
445
+ });
446
+
447
+ it('defaults suggestions passed to CarouselCardButtons to [] when card.suggestions is absent', () => {
448
+ render(<CarouselCard {...baseProps} card={{ mediaType: 'IMAGE' }} />);
449
+ expect(global.__lastButtonsProps.suggestions).toEqual([]);
450
+ });
451
+
452
+ it('defaults injectedTags to {} for the title/description TagList when injectedTags is explicitly null (bypassing defaultProps, hitting the `|| {}` fallback)', () => {
453
+ render(<CarouselCard {...baseProps} isEditFlow injectedTags={null} />);
454
+ const tagLists = screen.getAllByTestId('tag-list');
455
+ tagLists.forEach((el) => {
456
+ expect(el.getAttribute('data-injected-tags')).toBe('{}');
457
+ });
458
+ });
459
+
460
+ it('invokes the default no-op onContextChange without throwing when onContextChange is not supplied', () => {
461
+ render(<CarouselCard {...baseProps} isEditFlow onContextChange={undefined} />);
462
+ expect(() => global.__lastTagListProps.onContextChange()).not.toThrow();
463
+ });
464
+ });
@@ -0,0 +1,211 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ import React from 'react';
5
+ import { render, screen } from '@testing-library/react';
6
+ import '@testing-library/jest-dom';
7
+ import CarouselCardButtons from '../components/CarouselCardButtons';
8
+ import { MAX_BUTTONS } from '../constants';
9
+
10
+ jest.mock('@capillarytech/cap-ui-library/CapRow', () => {
11
+ const React = require('react');
12
+ return function CapRow(props) {
13
+ return React.createElement('div', { 'data-testid': 'cap-row' }, props?.children);
14
+ };
15
+ });
16
+ jest.mock('@capillarytech/cap-ui-library/CapHeader', () => {
17
+ const React = require('react');
18
+ return function CapHeader(props) {
19
+ return React.createElement('div', { 'data-testid': 'cap-header' }, props?.title, props?.description);
20
+ };
21
+ });
22
+ jest.mock('@capillarytech/cap-ui-library/CapHeading', () => {
23
+ const React = require('react');
24
+ return function CapHeading(props) {
25
+ return React.createElement('h4', null, props?.children);
26
+ };
27
+ });
28
+ jest.mock('@capillarytech/cap-ui-library/CapLabel', () => {
29
+ const React = require('react');
30
+ return function CapLabel(props) {
31
+ return React.createElement('div', { 'data-testid': 'cap-label' }, props?.children);
32
+ };
33
+ });
34
+
35
+ jest.mock('../../../v2Components/CapActionButton', () => {
36
+ const React = require('react');
37
+ return function MockCapActionButton(props) {
38
+ global.__lastActionButtonProps = props;
39
+ return React.createElement('div', { 'data-testid': 'cap-action-button' }, 'action-button');
40
+ };
41
+ });
42
+
43
+ const baseProps = {
44
+ cardIndex: 1,
45
+ suggestions: [
46
+ { index: 0, text: 'A' },
47
+ { index: 1, text: 'B' },
48
+ ],
49
+ isEditFlow: false,
50
+ isFullMode: true,
51
+ hostName: 'host-1',
52
+ formatMessage: jest.fn((msg) => msg?.defaultMessage || msg?.id || String(msg)),
53
+ onSuggestionsChange: jest.fn(),
54
+ };
55
+
56
+ describe('CarouselCardButtons', () => {
57
+ beforeEach(() => {
58
+ jest.clearAllMocks();
59
+ global.__lastActionButtonProps = undefined;
60
+ });
61
+
62
+ it('renders the CapHeader with the buttons label', () => {
63
+ render(<CarouselCardButtons {...baseProps} />);
64
+ expect(screen.getByTestId('cap-header')).toBeInTheDocument();
65
+ });
66
+
67
+ it('renders btnDesc label when isEditFlow is false', () => {
68
+ render(<CarouselCardButtons {...baseProps} isEditFlow={false} />);
69
+ expect(screen.getByTestId('cap-label')).toBeInTheDocument();
70
+ });
71
+
72
+ it('hides btnDesc label when isEditFlow is true', () => {
73
+ render(<CarouselCardButtons {...baseProps} isEditFlow />);
74
+ expect(screen.queryByTestId('cap-label')).not.toBeInTheDocument();
75
+ });
76
+
77
+ it('passes suggestions, isEditFlow, isFullMode, host and maxButtons to CapActionButton', () => {
78
+ render(<CarouselCardButtons {...baseProps} />);
79
+ const props = global.__lastActionButtonProps;
80
+ expect(props.suggestions).toEqual(baseProps.suggestions);
81
+ expect(props.isEditFlow).toBe(false);
82
+ expect(props.isFullMode).toBe(true);
83
+ expect(props.host).toBe('host-1');
84
+ expect(props.maxButtons).toBe(MAX_BUTTONS);
85
+ });
86
+
87
+ it('defaults suggestions to [] when suggestions prop is not provided', () => {
88
+ render(<CarouselCardButtons {...baseProps} suggestions={undefined} />);
89
+ const props = global.__lastActionButtonProps;
90
+ expect(props.suggestions).toEqual([]);
91
+ });
92
+
93
+ it('defaults suggestions to [] when suggestions is explicitly null (bypassing defaultProps, hitting the `|| []` fallback)', () => {
94
+ render(<CarouselCardButtons {...baseProps} suggestions={null} />);
95
+ const props = global.__lastActionButtonProps;
96
+ expect(props.suggestions).toEqual([]);
97
+ });
98
+
99
+ describe('minSavedSuggestions / hideDeleteSuggestionIndexes by cardIndex', () => {
100
+ it('sets minSavedSuggestions=1 and hideDeleteSuggestionIndexes=[0] when cardIndex === 0', () => {
101
+ render(<CarouselCardButtons {...baseProps} cardIndex={0} />);
102
+ const props = global.__lastActionButtonProps;
103
+ expect(props.minSavedSuggestions).toBe(1);
104
+ expect(props.hideDeleteSuggestionIndexes).toEqual([0]);
105
+ });
106
+
107
+ it('sets minSavedSuggestions=0 and hideDeleteSuggestionIndexes=[] when cardIndex !== 0', () => {
108
+ render(<CarouselCardButtons {...baseProps} cardIndex={2} />);
109
+ const props = global.__lastActionButtonProps;
110
+ expect(props.minSavedSuggestions).toBe(0);
111
+ expect(props.hideDeleteSuggestionIndexes).toEqual([]);
112
+ });
113
+ });
114
+
115
+ describe('updateButtonChange handler', () => {
116
+ it('calls onSuggestionsChange(data) directly when btnIndex === MAX_BUTTONS', () => {
117
+ render(<CarouselCardButtons {...baseProps} />);
118
+ const { updateButtonChange } = global.__lastActionButtonProps;
119
+ const data = { foo: 'bar' };
120
+ updateButtonChange(data, MAX_BUTTONS);
121
+ expect(baseProps.onSuggestionsChange).toHaveBeenCalledWith(data);
122
+ });
123
+
124
+ it('updates the suggestion at btnIndex and calls onSuggestionsChange with the full updated array for a normal index', () => {
125
+ render(<CarouselCardButtons {...baseProps} />);
126
+ const { updateButtonChange } = global.__lastActionButtonProps;
127
+ const data = { index: 0, text: 'Updated' };
128
+ updateButtonChange(data, 0);
129
+ expect(baseProps.onSuggestionsChange).toHaveBeenCalledWith([
130
+ { index: 0, text: 'Updated' },
131
+ { index: 1, text: 'B' },
132
+ ]);
133
+ });
134
+ });
135
+
136
+ describe('deleteButtonHandler', () => {
137
+ it('is a no-op when cardIndex === 0 && btnIndex === 0', () => {
138
+ render(<CarouselCardButtons {...baseProps} cardIndex={0} />);
139
+ const { deleteButtonHandler } = global.__lastActionButtonProps;
140
+ deleteButtonHandler(0);
141
+ expect(baseProps.onSuggestionsChange).not.toHaveBeenCalled();
142
+ });
143
+
144
+ it('is a no-op when cardIndex === 0 and the target suggestion isSaved with savedCount <= 1', () => {
145
+ const suggestions = [
146
+ { index: 0, isSaved: false },
147
+ { index: 1, isSaved: true },
148
+ ];
149
+ render(<CarouselCardButtons {...baseProps} cardIndex={0} suggestions={suggestions} />);
150
+ const { deleteButtonHandler } = global.__lastActionButtonProps;
151
+ deleteButtonHandler(1);
152
+ expect(baseProps.onSuggestionsChange).not.toHaveBeenCalled();
153
+ });
154
+
155
+ it('deletes and reindexes suggestions on the normal delete path (cardIndex !== 0)', () => {
156
+ const suggestions = [
157
+ { index: 0, text: 'A' },
158
+ { index: 1, text: 'B' },
159
+ { index: 2, text: 'C' },
160
+ ];
161
+ render(<CarouselCardButtons {...baseProps} cardIndex={2} suggestions={suggestions} />);
162
+ const { deleteButtonHandler } = global.__lastActionButtonProps;
163
+ deleteButtonHandler(1);
164
+ expect(baseProps.onSuggestionsChange).toHaveBeenCalledWith([
165
+ { index: 0, text: 'A' },
166
+ { index: 1, text: 'C' },
167
+ ]);
168
+ });
169
+
170
+ it('deletes on the normal path even for cardIndex === 0 when target is not isSaved (savedCount check not blocking)', () => {
171
+ const suggestions = [
172
+ { index: 0, isSaved: true },
173
+ { index: 1, isSaved: false },
174
+ { index: 2, isSaved: true },
175
+ ];
176
+ render(<CarouselCardButtons {...baseProps} cardIndex={0} suggestions={suggestions} />);
177
+ const { deleteButtonHandler } = global.__lastActionButtonProps;
178
+ deleteButtonHandler(1);
179
+ expect(baseProps.onSuggestionsChange).toHaveBeenCalledWith([
180
+ { index: 0, isSaved: true },
181
+ { index: 1, isSaved: true },
182
+ ]);
183
+ });
184
+
185
+ it('deletes on cardIndex === 0 when target isSaved but savedCount > 1 (not blocked)', () => {
186
+ const suggestions = [
187
+ { index: 0, isSaved: true },
188
+ { index: 1, isSaved: true },
189
+ { index: 2, isSaved: true },
190
+ ];
191
+ render(<CarouselCardButtons {...baseProps} cardIndex={0} suggestions={suggestions} />);
192
+ const { deleteButtonHandler } = global.__lastActionButtonProps;
193
+ deleteButtonHandler(1);
194
+ expect(baseProps.onSuggestionsChange).toHaveBeenCalledWith([
195
+ { index: 0, isSaved: true },
196
+ { index: 1, isSaved: true },
197
+ ]);
198
+ });
199
+
200
+ it('handles a missing target (find returns undefined) on cardIndex === 0 without throwing', () => {
201
+ const suggestions = [
202
+ { index: 0, isSaved: true },
203
+ { index: 2, isSaved: true },
204
+ ];
205
+ render(<CarouselCardButtons {...baseProps} cardIndex={0} suggestions={suggestions} />);
206
+ const { deleteButtonHandler } = global.__lastActionButtonProps;
207
+ expect(() => deleteButtonHandler(1)).not.toThrow();
208
+ expect(baseProps.onSuggestionsChange).toHaveBeenCalled();
209
+ });
210
+ });
211
+ });