@capillarytech/creatives-library 8.0.316-alpha.2 → 8.0.316-alpha.4

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 (107) hide show
  1. package/constants/unified.js +14 -0
  2. package/package.json +1 -1
  3. package/utils/templateVarUtils.js +172 -0
  4. package/utils/tests/templateVarUtils.test.js +160 -0
  5. package/v2Components/CapTagList/index.js +10 -0
  6. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
  7. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  8. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
  9. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  10. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  11. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  12. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  13. package/v2Components/CommonTestAndPreview/SendTestMessage.js +11 -5
  14. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +20 -1
  15. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  16. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +12 -0
  17. package/v2Components/CommonTestAndPreview/constants.js +38 -0
  18. package/v2Components/CommonTestAndPreview/index.js +693 -155
  19. package/v2Components/CommonTestAndPreview/messages.js +41 -3
  20. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  21. package/v2Components/CommonTestAndPreview/sagas.js +15 -6
  22. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +172 -0
  23. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +269 -1
  24. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  25. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +245 -0
  26. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +25 -4
  27. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +100 -1
  28. package/v2Components/CommonTestAndPreview/tests/index.test.js +19 -1
  29. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  30. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
  31. package/v2Components/FormBuilder/index.js +13 -13
  32. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
  33. package/v2Components/SmsFallback/constants.js +73 -0
  34. package/v2Components/SmsFallback/index.js +956 -0
  35. package/v2Components/SmsFallback/index.scss +265 -0
  36. package/v2Components/SmsFallback/messages.js +78 -0
  37. package/v2Components/SmsFallback/smsFallbackUtils.js +107 -0
  38. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  39. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  40. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  41. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
  42. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +261 -0
  43. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +327 -0
  44. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  45. package/v2Components/TestAndPreviewSlidebox/index.js +8 -1
  46. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  47. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  48. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  49. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  50. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  51. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  52. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  53. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  54. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  55. package/v2Containers/CreativesContainer/constants.js +9 -0
  56. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
  57. package/v2Containers/CreativesContainer/index.js +286 -93
  58. package/v2Containers/CreativesContainer/index.scss +51 -1
  59. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  60. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  61. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -10
  62. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  63. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  64. package/v2Containers/InApp/index.js +4 -5
  65. package/v2Containers/Rcs/constants.js +32 -1
  66. package/v2Containers/Rcs/index.js +953 -882
  67. package/v2Containers/Rcs/index.scss +85 -6
  68. package/v2Containers/Rcs/messages.js +10 -1
  69. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +205 -0
  70. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +40834 -1963
  71. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  72. package/v2Containers/Rcs/tests/index.test.js +41 -38
  73. package/v2Containers/Rcs/tests/mockData.js +38 -0
  74. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +251 -0
  75. package/v2Containers/Rcs/tests/utils.test.js +379 -1
  76. package/v2Containers/Rcs/utils.js +358 -10
  77. package/v2Containers/Sms/Create/index.js +81 -36
  78. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  79. package/v2Containers/SmsTrai/Create/index.js +9 -4
  80. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  81. package/v2Containers/SmsTrai/Edit/index.js +609 -128
  82. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  83. package/v2Containers/SmsTrai/Edit/messages.js +9 -4
  84. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4327 -2374
  85. package/v2Containers/SmsWrapper/index.js +37 -8
  86. package/v2Containers/TagList/index.js +6 -0
  87. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  88. package/v2Containers/Templates/_templates.scss +61 -2
  89. package/v2Containers/Templates/actions.js +11 -0
  90. package/v2Containers/Templates/constants.js +2 -0
  91. package/v2Containers/Templates/index.js +90 -40
  92. package/v2Containers/Templates/sagas.js +57 -12
  93. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  94. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1043 -1079
  95. package/v2Containers/Templates/tests/sagas.test.js +110 -12
  96. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  97. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  98. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  99. package/v2Containers/TemplatesV2/index.js +86 -23
  100. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  101. package/v2Containers/Viber/index.js +4 -9
  102. package/v2Containers/WebPush/Create/components/MessageSection.js +54 -18
  103. package/v2Containers/WebPush/Create/components/MessageSection.test.js +28 -0
  104. package/v2Containers/WebPush/Create/components/__snapshots__/MessageSection.test.js.snap +7 -3
  105. package/v2Containers/Whatsapp/index.js +10 -32
  106. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
  107. package/v2Containers/Whatsapp/tests/index.test.js +172 -0
@@ -24,6 +24,20 @@ export default defineMessages({
24
24
  id: `${scope}.personalizationTags`,
25
25
  defaultMessage: 'Personalization Tags',
26
26
  },
27
+ rcsTagsSectionTitle: {
28
+ id: `${scope}.rcsTagsSectionTitle`,
29
+ defaultMessage: 'RCS Tags',
30
+ },
31
+ /** Primary SMS section when channel is SMS — `messages[`${channel}TagsSectionTitle`]` resolves to this. */
32
+ SMSTagsSectionTitle: {
33
+ id: `${scope}.SMSTagsSectionTitle`,
34
+ defaultMessage: 'SMS tags',
35
+ },
36
+ /** Fallback SMS under RCS — same label as SMS Test & Preview for consistent personalization UX. */
37
+ smsFallbackTagsSectionTitle: {
38
+ id: `${scope}.smsFallbackTagsSectionTitle`,
39
+ defaultMessage: 'SMS tags',
40
+ },
27
41
  customValues: {
28
42
  id: `${scope}.customValues`,
29
43
  defaultMessage: 'Custom Values',
@@ -36,6 +50,14 @@ export default defineMessages({
36
50
  id: `${scope}.discardCustomValues`,
37
51
  defaultMessage: 'Discard custom values',
38
52
  },
53
+ rcsSenderIdLabel: {
54
+ id: `${scope}.rcsSenderIdLabel`,
55
+ defaultMessage: 'RCS Sender ID',
56
+ },
57
+ fallbackSmsSenderIdLabel: {
58
+ id: `${scope}.fallbackSmsSenderIdLabel`,
59
+ defaultMessage: 'Fallback SMS Sender ID',
60
+ },
39
61
  updatePreview: {
40
62
  id: `${scope}.updatePreview`,
41
63
  defaultMessage: 'Update Preview',
@@ -92,6 +114,14 @@ export default defineMessages({
92
114
  id: `${scope}.previewTitle`,
93
115
  defaultMessage: 'Preview',
94
116
  },
117
+ rcsTab: {
118
+ id: `${scope}.rcsTab`,
119
+ defaultMessage: 'RCS',
120
+ },
121
+ smsFallbackTab: {
122
+ id: `${scope}.smsFallbackTab`,
123
+ defaultMessage: 'Fallback SMS',
124
+ },
95
125
  previewPlaceholder: {
96
126
  id: `${scope}.previewPlaceholder`,
97
127
  defaultMessage: 'Click "Update Preview" to see the rendered email.',
@@ -180,14 +210,22 @@ export default defineMessages({
180
210
  id: `${scope}.lastModified`,
181
211
  defaultMessage: 'Last modified',
182
212
  },
183
- by: {
184
- id: `${scope}.by`,
185
- defaultMessage: 'by',
213
+ byAuthor: {
214
+ id: `${scope}.byAuthor`,
215
+ defaultMessage: 'by {name}',
186
216
  },
187
217
  senderId: {
188
218
  id: `${scope}.senderId`,
189
219
  defaultMessage: 'Sender ID',
190
220
  },
221
+ rcsSenderIdLabel: {
222
+ id: `${scope}.rcsSenderIdLabel`,
223
+ defaultMessage: 'RCS sender ID',
224
+ },
225
+ domainLabel: {
226
+ id: `${scope}.domainLabel`,
227
+ defaultMessage: 'Domain',
228
+ },
191
229
  urlPreviewImage: {
192
230
  id: `${scope}.urlPreviewImage`,
193
231
  defaultMessage: 'URL Preview Image',
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Helpers for Liquid `/preview` (updateEmailPreview) responses.
3
+ * The API may nest the payload under `data` or return fields at the top level.
4
+ * SMS responses often use `messageBody` instead of `resolvedBody`.
5
+ */
6
+
7
+ import { getFallbackResolvedContent } from '../../utils/templateVarUtils';
8
+ import { RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from './constants';
9
+
10
+ export function normalizePreviewApiPayload(raw) {
11
+ if (!raw || typeof raw !== 'object') return raw;
12
+ const next = { ...raw };
13
+ if (next.resolvedBody == null) {
14
+ const alt = next.messageBody ?? next.previewMessage ?? next.previewText;
15
+ if (alt != null) next.resolvedBody = alt;
16
+ }
17
+ return next;
18
+ }
19
+
20
+ /**
21
+ * Returns a normalized preview object suitable for Redux `previewData`, or null on error / empty.
22
+ */
23
+ export function extractPreviewFromLiquidResponse(response) {
24
+ if (!response || response.error) return null;
25
+ if (response.errors && Array.isArray(response.errors) && response.errors.length > 0) return null;
26
+
27
+ let body;
28
+ if (response.data !== undefined && response.data !== null && typeof response.data === 'object') {
29
+ body = normalizePreviewApiPayload(response.data);
30
+ } else if (typeof response === 'object' && !Array.isArray(response)) {
31
+ body = normalizePreviewApiPayload(response);
32
+ } else {
33
+ return null;
34
+ }
35
+
36
+ if (!body) return null;
37
+ if (body.resolvedBody === undefined && body.messageBody === undefined) {
38
+ return null;
39
+ }
40
+ return body;
41
+ }
42
+
43
+ /**
44
+ * RCS SMS fallback: merge template (`templateContent` / `content`) with VarSegment
45
+ * `rcsSmsFallbackVarMapped` before Liquid /preview, tag extraction, or createMessageMeta.
46
+ * Raw template alone is stale when the user edits slot values.
47
+ */
48
+ export function getSmsFallbackTextForTagExtraction(smsFallbackContext) {
49
+ const rawTemplateBody =
50
+ smsFallbackContext?.templateContent ?? smsFallbackContext?.content ?? '';
51
+ if (!rawTemplateBody) return '';
52
+ const rcsSmsFallbackVarMapped = smsFallbackContext?.[RCS_SMS_FALLBACK_VAR_MAPPED_PROP];
53
+ const hasRcsSmsFallbackVarMappedEntries =
54
+ Object.keys(rcsSmsFallbackVarMapped ?? {}).length > 0;
55
+ if (hasRcsSmsFallbackVarMappedEntries) {
56
+ return getFallbackResolvedContent(rawTemplateBody, rcsSmsFallbackVarMapped ?? {});
57
+ }
58
+ return rawTemplateBody;
59
+ }
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import {
6
- call, put, takeLatest, all,
6
+ call, put, takeLatest, takeEvery, all,
7
7
  } from 'redux-saga/effects';
8
8
  import get from 'lodash/get';
9
9
  import isEmpty from 'lodash/isEmpty';
@@ -45,6 +45,7 @@ import {
45
45
  CHANNELS,
46
46
  } from './constants';
47
47
  import { parseSenderDetailsResponse } from './DeliverySettings/utils/parseSenderDetailsResponse';
48
+ import { extractPreviewFromLiquidResponse } from './previewApiUtils';
48
49
 
49
50
  // Search Customers Saga
50
51
  export function* searchCustomersSaga(action) {
@@ -91,11 +92,12 @@ export function* updatePreviewSaga(action) {
91
92
  const customValues = action.payload.resolvedTags;
92
93
 
93
94
  const response = yield call(Api.updateEmailPreview, action.payload);
94
- if (response?.data) {
95
+ const previewPayload = extractPreviewFromLiquidResponse(response);
96
+ if (previewPayload) {
95
97
  yield put({
96
98
  type: UPDATE_PREVIEW_SUCCESS,
97
99
  payload: {
98
- previewData: response.data,
100
+ previewData: previewPayload,
99
101
  customValues, // Pass custom values to be preserved
100
102
  },
101
103
  });
@@ -236,8 +238,13 @@ export function* createMessageMetaSaga(action) {
236
238
  export function* getPrefilledValuesSaga(action) {
237
239
  try {
238
240
  const response = yield call(Api.updateEmailPreview, action.payload);
239
- if (response?.data) {
240
- yield put({ type: GET_PREFILLED_VALUES_SUCCESS, payload: { values: response?.data?.resolvedTagValues } });
241
+ const body =
242
+ response?.data !== undefined && response?.data !== null
243
+ ? response.data
244
+ : response;
245
+ const resolvedTagValues = body?.resolvedTagValues;
246
+ if (resolvedTagValues != null) {
247
+ yield put({ type: GET_PREFILLED_VALUES_SUCCESS, payload: { values: resolvedTagValues } });
241
248
  } else {
242
249
  // Pass all errors from API response to state
243
250
  yield put({
@@ -337,7 +344,9 @@ export function* getWeCrmAccountsSaga(action) {
337
344
  }
338
345
 
339
346
  export function* watchGetSenderDetails() {
340
- yield takeLatest(GET_SENDER_DETAILS_REQUESTED, getSenderDetailsSaga);
347
+ // takeEvery: RCS test & preview dispatches RCS then SMS back-to-back; takeLatest would
348
+ // cancel the RCS fetch so senderDetailsByChannel.RCS stayed empty while SMS loaded.
349
+ yield takeEvery(GET_SENDER_DETAILS_REQUESTED, getSenderDetailsSaga);
341
350
  }
342
351
 
343
352
  export function* watchGetWeCrmAccounts() {
@@ -0,0 +1,172 @@
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 { IntlProvider } from 'react-intl';
8
+ import CustomValuesEditor from '../CustomValuesEditor';
9
+
10
+ jest.mock('@capillarytech/cap-ui-library/CapRow', () => {
11
+ const React = require('react');
12
+ return function CapRow(props) {
13
+ return React.createElement('div', { className: props?.className, 'data-testid': 'cap-row' }, props?.children);
14
+ };
15
+ });
16
+ jest.mock('@capillarytech/cap-ui-library/CapSpin', () => {
17
+ const React = require('react');
18
+ return function CapSpin() {
19
+ return React.createElement('div', { 'data-testid': 'cap-spin' }, 'spin');
20
+ };
21
+ });
22
+ jest.mock('@capillarytech/cap-ui-library/CapSwitch', () => {
23
+ const React = require('react');
24
+ return function CapSwitch(props) {
25
+ return React.createElement('input', {
26
+ type: 'checkbox',
27
+ 'data-testid': 'json-switch',
28
+ checked: props?.checked,
29
+ onChange: (e) => props?.onChange?.(e.target.checked),
30
+ });
31
+ };
32
+ });
33
+ jest.mock('@capillarytech/cap-ui-library/CapButton', () => {
34
+ const React = require('react');
35
+ return function CapButton(props) {
36
+ return React.createElement('button', {
37
+ type: 'button',
38
+ 'data-testid': 'cap-button',
39
+ onClick: props?.onClick,
40
+ disabled: props?.disabled,
41
+ 'data-loading': props?.loading,
42
+ }, props?.children);
43
+ };
44
+ });
45
+ jest.mock('@capillarytech/cap-ui-library/CapInput', () => {
46
+ const React = require('react');
47
+ return function CapInput(props) {
48
+ return React.createElement('input', {
49
+ 'data-testid': 'tag-input',
50
+ value: props?.value,
51
+ placeholder: props?.placeholder,
52
+ onChange: props?.onChange,
53
+ });
54
+ };
55
+ });
56
+ jest.mock('@capillarytech/cap-ui-library/CapLabel', () => {
57
+ const React = require('react');
58
+ return function CapLabel(props) {
59
+ return React.createElement('div', { className: props?.className, 'data-testid': 'cap-label' }, props?.children);
60
+ };
61
+ });
62
+
63
+ const formatMessage = (msg) => (typeof msg === 'object' && msg?.id ? msg.id : String(msg));
64
+
65
+ const baseProps = {
66
+ isExtractingTags: false,
67
+ isUpdatePreviewDisabled: false,
68
+ showJSON: false,
69
+ setShowJSON: jest.fn(),
70
+ customValues: { 'tag.a': 'v1' },
71
+ handleJSONTextChange: jest.fn(),
72
+ sections: [
73
+ {
74
+ key: 'sec1',
75
+ title: 'Section A',
76
+ requiredTags: [{ fullPath: 'tag.a', name: 'a' }],
77
+ optionalTags: [{ fullPath: 'tag.b', name: 'b' }],
78
+ },
79
+ ],
80
+ handleCustomValueChange: jest.fn(),
81
+ handleDiscardCustomValues: jest.fn(),
82
+ handleUpdatePreview: jest.fn(),
83
+ isUpdatingPreview: false,
84
+ formatMessage,
85
+ };
86
+
87
+ describe('CustomValuesEditor', () => {
88
+ beforeEach(() => {
89
+ jest.clearAllMocks();
90
+ });
91
+
92
+ it('shows loading state when isExtractingTags', () => {
93
+ render(
94
+ <IntlProvider locale="en" messages={{}}>
95
+ <CustomValuesEditor {...baseProps} isExtractingTags />
96
+ </IntlProvider>,
97
+ );
98
+ expect(screen.getByTestId('cap-spin')).toBeInTheDocument();
99
+ });
100
+
101
+ it('shows values missing label when isUpdatePreviewDisabled', () => {
102
+ render(
103
+ <IntlProvider locale="en" messages={{}}>
104
+ <CustomValuesEditor {...baseProps} isUpdatePreviewDisabled />
105
+ </IntlProvider>,
106
+ );
107
+ expect(document.querySelector('.values-missing-message')).toBeTruthy();
108
+ });
109
+
110
+ it('toggles JSON mode and calls setShowJSON', () => {
111
+ const setShowJSON = jest.fn();
112
+ render(
113
+ <IntlProvider locale="en" messages={{}}>
114
+ <CustomValuesEditor {...baseProps} setShowJSON={setShowJSON} />
115
+ </IntlProvider>,
116
+ );
117
+ fireEvent.click(screen.getByTestId('json-switch'));
118
+ expect(setShowJSON).toHaveBeenCalled();
119
+ });
120
+
121
+ it('renders JSON textarea and fires handleJSONTextChange', () => {
122
+ render(
123
+ <IntlProvider locale="en" messages={{}}>
124
+ <CustomValuesEditor {...baseProps} showJSON />
125
+ </IntlProvider>,
126
+ );
127
+ const ta = document.querySelector('.json-textarea');
128
+ expect(ta).toBeTruthy();
129
+ fireEvent.change(ta, { target: { value: '{}' } });
130
+ expect(baseProps.handleJSONTextChange).toHaveBeenCalledWith('{}');
131
+ });
132
+
133
+ it('renders required and optional tag inputs when not JSON', () => {
134
+ render(
135
+ <IntlProvider locale="en" messages={{}}>
136
+ <CustomValuesEditor {...baseProps} />
137
+ </IntlProvider>,
138
+ );
139
+ const inputs = screen.getAllByTestId('tag-input');
140
+ expect(inputs.length).toBeGreaterThan(0);
141
+ fireEvent.change(inputs[0], { target: { value: 'new' } });
142
+ expect(baseProps.handleCustomValueChange).toHaveBeenCalledWith('tag.a', 'new');
143
+ });
144
+
145
+ it('calls discard and update handlers', () => {
146
+ render(
147
+ <IntlProvider locale="en" messages={{}}>
148
+ <CustomValuesEditor {...baseProps} />
149
+ </IntlProvider>,
150
+ );
151
+ const buttons = screen.getAllByTestId('cap-button');
152
+ fireEvent.click(buttons[0]);
153
+ fireEvent.click(buttons[buttons.length - 1]);
154
+ expect(baseProps.handleDiscardCustomValues).toHaveBeenCalled();
155
+ expect(baseProps.handleUpdatePreview).toHaveBeenCalled();
156
+ });
157
+
158
+ it('skips sections with no tags', () => {
159
+ render(
160
+ <IntlProvider locale="en" messages={{}}>
161
+ <CustomValuesEditor
162
+ {...baseProps}
163
+ sections={[
164
+ { key: 'empty', requiredTags: [], optionalTags: [] },
165
+ baseProps.sections[0],
166
+ ]}
167
+ />
168
+ </IntlProvider>,
169
+ );
170
+ expect(screen.getAllByTestId('tag-input').length).toBeGreaterThan(0);
171
+ });
172
+ });
@@ -187,7 +187,7 @@ describe('ModifyDeliverySettings', () => {
187
187
  const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
188
188
 
189
189
  renderComponent({
190
- channel: 'RCS',
190
+ channel: 'UNSUPPORTED_CHANNEL',
191
191
  onSaveDeliverySettings: onSave,
192
192
  });
193
193
 
@@ -614,6 +614,22 @@ describe('ModifyDeliverySettings', () => {
614
614
  expect(senderNameOptions).toEqual(expect.arrayContaining(['fallback@test.com']));
615
615
  expect(replyToOptions).toEqual(expect.arrayContaining(['Reply Label']));
616
616
  });
617
+
618
+ it('should use reply-to label when value is empty in reply-to options', () => {
619
+ renderComponent({
620
+ channel: CHANNELS.EMAIL,
621
+ senderDetailsOptions: [{
622
+ domainId: 90,
623
+ domainName: 'Reply Label Domain',
624
+ emailSenders: [{ value: 'a@test.com' }],
625
+ emailRepliers: [{ label: 'OnlyLabel', value: '' }],
626
+ }],
627
+ deliverySettings: { domainId: 90, domainGatewayMapId: 1 },
628
+ });
629
+
630
+ const replyToOptions = Array.from(getSelects()[3].querySelectorAll('option')).map((opt) => opt.textContent);
631
+ expect(replyToOptions).toEqual(expect.arrayContaining(['OnlyLabel']));
632
+ });
617
633
  });
618
634
 
619
635
  describe('WHATSAPP flow', () => {
@@ -838,6 +854,19 @@ describe('ModifyDeliverySettings', () => {
838
854
  sourceAccountIdentifier: 'waba-1',
839
855
  });
840
856
  });
857
+
858
+ it('should render disabled CapLabel with label3 for locked WhatsApp account row', () => {
859
+ renderComponent({
860
+ channel: CHANNELS.WHATSAPP,
861
+ senderDetailsOptions: whatsappDomains,
862
+ wecrmAccounts,
863
+ whatsappAccountFromForm: { accountName: 'Account One' },
864
+ });
865
+
866
+ const labels = screen.getAllByTestId('cap-label');
867
+ const disabledHint = labels.find((el) => el.getAttribute('data-type') === 'label3');
868
+ expect(disabledHint).toBeTruthy();
869
+ });
841
870
  });
842
871
 
843
872
  describe('Done and close handling', () => {
@@ -885,4 +914,243 @@ describe('ModifyDeliverySettings', () => {
885
914
  expect(onSave).toHaveBeenCalled();
886
915
  });
887
916
  });
917
+
918
+ describe('RCS flow', () => {
919
+ const rcsDomains = [
920
+ {
921
+ domainId: 51,
922
+ domainName: 'RCS Domain A',
923
+ dgmId: 511,
924
+ gsmSenders: [{ value: 'RCS_GSM_1' }, { value: 'RCS_GSM_2' }],
925
+ },
926
+ {
927
+ domainId: 52,
928
+ domainName: 'RCS Domain B',
929
+ dgmId: 522,
930
+ gsmSenders: [{ value: 'RCS_OTHER' }],
931
+ },
932
+ ];
933
+
934
+ const smsFallbackDomains = [
935
+ {
936
+ domainId: 61,
937
+ domainName: 'SMS Fallback',
938
+ gsmSenders: [{ value: 'SMS_FB_1' }],
939
+ },
940
+ ];
941
+
942
+ it('should render RCS domain and sender selects', () => {
943
+ renderComponent({
944
+ channel: CHANNELS.RCS,
945
+ senderDetailsOptions: rcsDomains,
946
+ });
947
+ expect(getSelects()).toHaveLength(2);
948
+ });
949
+
950
+ it('should set composite gsm sender id when RCS domain changes', () => {
951
+ const onSave = jest.fn();
952
+
953
+ renderComponent({
954
+ channel: CHANNELS.RCS,
955
+ senderDetailsOptions: rcsDomains,
956
+ onSaveDeliverySettings: onSave,
957
+ });
958
+
959
+ fireEvent.change(getSelects()[0], { target: { value: '51' } });
960
+ fireEvent.click(screen.getByText('Done'));
961
+
962
+ expect(onSave).toHaveBeenCalledWith(
963
+ expect.objectContaining({
964
+ domainId: 51,
965
+ gsmSenderId: '51|RCS_GSM_1',
966
+ }),
967
+ );
968
+ });
969
+
970
+ it('should render SMS fallback domain and sender when fallback options exist', () => {
971
+ renderComponent({
972
+ channel: CHANNELS.RCS,
973
+ senderDetailsOptions: rcsDomains,
974
+ smsFallbackSenderDetailsOptions: smsFallbackDomains,
975
+ });
976
+ expect(getSelects()).toHaveLength(4);
977
+ });
978
+
979
+ it('should strip gsm and fallback sender ids that are not in current options on Done', () => {
980
+ const onSave = jest.fn();
981
+
982
+ renderComponent({
983
+ channel: CHANNELS.RCS,
984
+ senderDetailsOptions: rcsDomains,
985
+ smsFallbackSenderDetailsOptions: smsFallbackDomains,
986
+ deliverySettings: {
987
+ domainId: 51,
988
+ gsmSenderId: 'not-a-valid-composite',
989
+ smsFallbackDomainId: 61,
990
+ cdmaSenderId: '61|SMS_FB_1',
991
+ },
992
+ onSaveDeliverySettings: onSave,
993
+ });
994
+
995
+ fireEvent.click(screen.getByText('Done'));
996
+
997
+ expect(onSave).toHaveBeenCalledWith(
998
+ expect.objectContaining({
999
+ domainId: 51,
1000
+ gsmSenderId: '',
1001
+ smsFallbackDomainId: 61,
1002
+ cdmaSenderId: '61|SMS_FB_1',
1003
+ }),
1004
+ );
1005
+ });
1006
+
1007
+ it('should strip invalid SMS fallback composite sender on Done', () => {
1008
+ const onSave = jest.fn();
1009
+
1010
+ renderComponent({
1011
+ channel: CHANNELS.RCS,
1012
+ senderDetailsOptions: rcsDomains,
1013
+ smsFallbackSenderDetailsOptions: smsFallbackDomains,
1014
+ deliverySettings: {
1015
+ domainId: 51,
1016
+ gsmSenderId: '51|RCS_GSM_1',
1017
+ smsFallbackDomainId: 61,
1018
+ cdmaSenderId: 'bogus|value',
1019
+ },
1020
+ onSaveDeliverySettings: onSave,
1021
+ });
1022
+
1023
+ fireEvent.click(screen.getByText('Done'));
1024
+
1025
+ expect(onSave).toHaveBeenCalledWith(
1026
+ expect.objectContaining({
1027
+ gsmSenderId: '51|RCS_GSM_1',
1028
+ cdmaSenderId: '',
1029
+ }),
1030
+ );
1031
+ });
1032
+
1033
+ it('should filter SMS fallback domains and GSM senders when TRAI DLT and registered sender IDs apply', () => {
1034
+ const fallbackDlt = [
1035
+ {
1036
+ domainId: 71,
1037
+ domainName: 'Has Registered GSM',
1038
+ gsmSenders: [{ value: 'REG_OK' }, { value: 'NOT_ON_DLT' }],
1039
+ },
1040
+ {
1041
+ domainId: 72,
1042
+ domainName: 'No Match',
1043
+ gsmSenders: [{ value: 'UNMATCHED' }],
1044
+ },
1045
+ ];
1046
+ const onSave = jest.fn();
1047
+
1048
+ renderComponent({
1049
+ channel: CHANNELS.RCS,
1050
+ senderDetailsOptions: rcsDomains,
1051
+ smsFallbackSenderDetailsOptions: fallbackDlt,
1052
+ smsTraiDltEnabled: true,
1053
+ registeredSenderIds: ['REG_OK'],
1054
+ deliverySettings: {
1055
+ domainId: 51,
1056
+ gsmSenderId: '51|RCS_GSM_1',
1057
+ smsFallbackDomainId: 71,
1058
+ cdmaSenderId: '71|REG_OK',
1059
+ },
1060
+ onSaveDeliverySettings: onSave,
1061
+ });
1062
+
1063
+ fireEvent.click(screen.getByText('Done'));
1064
+
1065
+ expect(onSave).toHaveBeenCalledWith(
1066
+ expect.objectContaining({
1067
+ smsFallbackDomainId: 71,
1068
+ cdmaSenderId: '71|REG_OK',
1069
+ }),
1070
+ );
1071
+ });
1072
+
1073
+ it('should set SMS fallback composite to first GSM after skipping domain-name echo row', () => {
1074
+ const fallbackEcho = [
1075
+ {
1076
+ domainId: 80,
1077
+ domainName: 'Echo',
1078
+ gsmSenders: [{ value: 'Echo' }, { value: 'REAL_SENDER' }],
1079
+ },
1080
+ ];
1081
+ const onSave = jest.fn();
1082
+
1083
+ renderComponent({
1084
+ channel: CHANNELS.RCS,
1085
+ senderDetailsOptions: rcsDomains,
1086
+ smsFallbackSenderDetailsOptions: fallbackEcho,
1087
+ deliverySettings: {
1088
+ domainId: 51,
1089
+ gsmSenderId: '51|RCS_GSM_1',
1090
+ },
1091
+ onSaveDeliverySettings: onSave,
1092
+ });
1093
+
1094
+ fireEvent.change(getSelects()[2], { target: { value: '80' } });
1095
+ fireEvent.click(screen.getByText('Done'));
1096
+
1097
+ expect(onSave).toHaveBeenCalledWith(
1098
+ expect.objectContaining({
1099
+ smsFallbackDomainId: 80,
1100
+ cdmaSenderId: '80|REAL_SENDER',
1101
+ }),
1102
+ );
1103
+ });
1104
+
1105
+ it('should only list RCS sender options for the selected RCS domain', () => {
1106
+ const onSave = jest.fn();
1107
+
1108
+ renderComponent({
1109
+ channel: CHANNELS.RCS,
1110
+ senderDetailsOptions: rcsDomains,
1111
+ onSaveDeliverySettings: onSave,
1112
+ deliverySettings: {
1113
+ domainId: 52,
1114
+ gsmSenderId: '52|RCS_OTHER',
1115
+ },
1116
+ });
1117
+
1118
+ fireEvent.click(screen.getByText('Done'));
1119
+
1120
+ expect(onSave).toHaveBeenCalledWith(
1121
+ expect.objectContaining({
1122
+ domainId: 52,
1123
+ gsmSenderId: '52|RCS_OTHER',
1124
+ }),
1125
+ );
1126
+ });
1127
+
1128
+ it('should set composite RCS gsm when domain id is numeric zero', () => {
1129
+ const rcsWithZero = [
1130
+ {
1131
+ domainId: 0,
1132
+ domainName: 'Zero Id',
1133
+ dgmId: 1,
1134
+ gsmSenders: [{ value: 'Z_GSM' }],
1135
+ },
1136
+ ];
1137
+ const onSave = jest.fn();
1138
+
1139
+ renderComponent({
1140
+ channel: CHANNELS.RCS,
1141
+ senderDetailsOptions: rcsWithZero,
1142
+ onSaveDeliverySettings: onSave,
1143
+ });
1144
+
1145
+ fireEvent.change(getSelects()[0], { target: { value: '0' } });
1146
+ fireEvent.click(screen.getByText('Done'));
1147
+
1148
+ expect(onSave).toHaveBeenCalledWith(
1149
+ expect.objectContaining({
1150
+ domainId: 0,
1151
+ gsmSenderId: '0|Z_GSM',
1152
+ }),
1153
+ );
1154
+ });
1155
+ });
888
1156
  });