@capillarytech/creatives-library 8.0.328 → 8.0.330-alpha.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.
- package/constants/unified.js +4 -0
- package/package.json +1 -1
- package/services/api.js +17 -0
- package/services/tests/api.test.js +85 -0
- package/utils/commonUtils.js +28 -0
- package/utils/tagValidations.js +2 -3
- package/utils/templateVarUtils.js +35 -6
- package/utils/tests/commonUtil.test.js +169 -0
- package/utils/tests/tagValidations.test.js +1 -1
- package/utils/tests/templateVarUtils.test.js +44 -0
- package/v2Components/CommonTestAndPreview/AddTestCustomer.js +42 -0
- package/v2Components/CommonTestAndPreview/CustomerCreationModal.js +155 -0
- package/v2Components/CommonTestAndPreview/ExistingCustomerModal.js +93 -0
- package/v2Components/CommonTestAndPreview/SendTestMessage.js +79 -51
- package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +134 -34
- package/v2Components/CommonTestAndPreview/actions.js +10 -0
- package/v2Components/CommonTestAndPreview/constants.js +15 -1
- package/v2Components/CommonTestAndPreview/index.js +364 -72
- package/v2Components/CommonTestAndPreview/messages.js +106 -0
- package/v2Components/CommonTestAndPreview/reducer.js +10 -0
- package/v2Components/CommonTestAndPreview/tests/AddTestCustomer.test.js +66 -0
- package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +648 -0
- package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +24 -0
- package/v2Components/CommonTestAndPreview/tests/CustomerCreationModal.test.js +174 -0
- package/v2Components/CommonTestAndPreview/tests/ExistingCustomerModal.test.js +114 -0
- package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +52 -0
- package/v2Components/CommonTestAndPreview/tests/constants.test.js +31 -1
- package/v2Components/CommonTestAndPreview/tests/index.test.js +36 -0
- package/v2Components/CommonTestAndPreview/tests/reducer.test.js +71 -0
- package/v2Components/CommonTestAndPreview/tests/selectors.test.js +17 -0
- package/v2Components/SmsFallback/smsFallbackUtils.js +14 -3
- package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +16 -0
- package/v2Components/TestAndPreviewSlidebox/index.js +5 -0
- package/v2Containers/CreativesContainer/index.js +15 -10
- package/v2Containers/Rcs/constants.js +6 -2
- package/v2Containers/Rcs/index.js +219 -91
- package/v2Containers/Rcs/messages.js +2 -1
- package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +20 -0
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +2370 -1758
- package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +67 -0
- package/v2Containers/Rcs/tests/utils.test.js +56 -0
- package/v2Containers/Rcs/utils.js +53 -6
- package/v2Containers/SmsTrai/Edit/index.js +27 -0
- package/v2Containers/SmsTrai/Edit/messages.js +5 -0
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +357 -324
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +5586 -5212
package/constants/unified.js
CHANGED
|
@@ -168,6 +168,10 @@ export const COMBINED_SMS_TEMPLATE_VAR_REGEX = /\{\{[^}]+\}\}|\{\#[^#]*\#\}/g;
|
|
|
168
168
|
export const MUSTACHE_VAR_TOKEN_FULL_STRING_REGEX = /^\{\{[^}]+\}\}$/;
|
|
169
169
|
/** Full-string check: one DLT hash token. */
|
|
170
170
|
export const DLT_HASH_VAR_TOKEN_FULL_STRING_REGEX = /^\{\#[^#]*\#\}$/;
|
|
171
|
+
/** Full-string with capture group: inner name for `{{ name }}`-style tokens (whitespace-tolerant). */
|
|
172
|
+
export const MUSTACHE_TOKEN_INNER_NAME_REGEX = /^\{\{\s*([^}]+?)\s*\}\}$/;
|
|
173
|
+
/** Full-string with capture group: inner name/body for `{# name #}` DLT tokens (whitespace-tolerant). */
|
|
174
|
+
export const DLT_HASH_TOKEN_INNER_NAME_REGEX = /^\{#\s*(.*?)\s*#\}$/;
|
|
171
175
|
/** Global with capture group: inner name inside `{{name}}`. */
|
|
172
176
|
export const MUSTACHE_VAR_NAME_CAPTURE_REGEX = /\{\{([^}]+)\}\}/g;
|
|
173
177
|
/** Global with capture group: inner body inside `{#body#}`. */
|
package/package.json
CHANGED
package/services/api.js
CHANGED
|
@@ -741,4 +741,21 @@ export const getBeePopupBuilderToken = () => {
|
|
|
741
741
|
return request(url, getAPICallObject('GET'));
|
|
742
742
|
};
|
|
743
743
|
|
|
744
|
+
/**
|
|
745
|
+
* Look up member by identifier (email or mobile) for add test customer flow.
|
|
746
|
+
* Uses standard API auth headers (no x-cap-ct) to avoid CORS preflight issues with node layer.
|
|
747
|
+
* @param {string} identifierType - 'email' or 'mobile'
|
|
748
|
+
* @param {string} identifierValue - email address or mobile number
|
|
749
|
+
* @returns {Promise} Promise resolving to { success, response: { exists, customerDetails } }
|
|
750
|
+
*/
|
|
751
|
+
export const getMembersLookup = (identifierType, identifierValue) => {
|
|
752
|
+
const url = `${API_ENDPOINT}/members?identifierType=${encodeURIComponent(identifierType)}&identifierValue=${encodeURIComponent(identifierValue)}`;
|
|
753
|
+
return request(url, getAPICallObject('GET'));
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
export const createTestCustomer = (payload) => {
|
|
757
|
+
const url = `${API_ENDPOINT}/testCustomers`;
|
|
758
|
+
return request(url, getAPICallObject('POST', payload));
|
|
759
|
+
};
|
|
760
|
+
|
|
744
761
|
export {request, getAPICallObject};
|
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
getAssetStatus,
|
|
30
30
|
getBeePopupBuilderToken,
|
|
31
31
|
getCmsAccounts,
|
|
32
|
+
getMembersLookup,
|
|
33
|
+
createTestCustomer,
|
|
32
34
|
} from '../api';
|
|
33
35
|
import { mockData } from './mockData';
|
|
34
36
|
import getSchema from '../getSchema';
|
|
@@ -1014,3 +1016,86 @@ describe('getCmsAccounts', () => {
|
|
|
1014
1016
|
expect(result).toBeInstanceOf(Promise);
|
|
1015
1017
|
});
|
|
1016
1018
|
});
|
|
1019
|
+
|
|
1020
|
+
describe('getMembersLookup', () => {
|
|
1021
|
+
beforeEach(() => {
|
|
1022
|
+
global.fetch = jest.fn();
|
|
1023
|
+
global.fetch.mockReturnValue(Promise.resolve({
|
|
1024
|
+
status: 200,
|
|
1025
|
+
json: () => Promise.resolve({ success: true, response: { exists: false, customerDetails: [] } }),
|
|
1026
|
+
}));
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
it('should return a Promise', () => {
|
|
1030
|
+
const result = getMembersLookup('email', 'user@example.com');
|
|
1031
|
+
expect(result).toBeInstanceOf(Promise);
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
it('should be callable with identifierType and identifierValue', () => {
|
|
1035
|
+
expect(typeof getMembersLookup).toBe('function');
|
|
1036
|
+
const result = getMembersLookup('mobile', '9123456789');
|
|
1037
|
+
expect(result).toBeInstanceOf(Promise);
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
it('should call fetch with correct URL encoding and GET method', () => {
|
|
1041
|
+
global.fetch.mockClear();
|
|
1042
|
+
getMembersLookup('email', 'user+test@example.com');
|
|
1043
|
+
expect(global.fetch).toHaveBeenCalled();
|
|
1044
|
+
const calls = global.fetch.mock.calls;
|
|
1045
|
+
const withEncoding = calls.find(
|
|
1046
|
+
(c) =>
|
|
1047
|
+
c[0] &&
|
|
1048
|
+
String(c[0]).includes('members') &&
|
|
1049
|
+
String(c[0]).includes('identifierType=email') &&
|
|
1050
|
+
String(c[0]).includes('user%2Btest%40example.com')
|
|
1051
|
+
);
|
|
1052
|
+
if (withEncoding) {
|
|
1053
|
+
const [url, options] = withEncoding;
|
|
1054
|
+
expect(url).toContain('identifierType=email');
|
|
1055
|
+
expect(url).toContain('identifierValue=user%2Btest%40example.com');
|
|
1056
|
+
expect(options?.method || 'GET').toBe('GET');
|
|
1057
|
+
}
|
|
1058
|
+
const anyMembersCall = calls.find((c) => c[0] && String(c[0]).includes('members'));
|
|
1059
|
+
expect(anyMembersCall).toBeDefined();
|
|
1060
|
+
expect(anyMembersCall[0]).toContain('identifierType=');
|
|
1061
|
+
expect(anyMembersCall[0]).toContain('identifierValue=');
|
|
1062
|
+
expect(anyMembersCall[1]?.method || 'GET').toBe('GET');
|
|
1063
|
+
});
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
describe('createTestCustomer', () => {
|
|
1067
|
+
beforeEach(() => {
|
|
1068
|
+
global.fetch = jest.fn();
|
|
1069
|
+
global.fetch.mockReturnValue(Promise.resolve({
|
|
1070
|
+
status: 200,
|
|
1071
|
+
json: () => Promise.resolve({ success: true }),
|
|
1072
|
+
}));
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
it('should return a Promise', () => {
|
|
1076
|
+
const payload = { customer: { email: 'test@example.com', firstName: 'Test' } };
|
|
1077
|
+
const result = createTestCustomer(payload);
|
|
1078
|
+
expect(result).toBeInstanceOf(Promise);
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
it('should accept customerId payload for existing customer', () => {
|
|
1082
|
+
const payload = { customerId: 'cust-123' };
|
|
1083
|
+
const result = createTestCustomer(payload);
|
|
1084
|
+
expect(result).toBeInstanceOf(Promise);
|
|
1085
|
+
expect(global.fetch).toHaveBeenCalled();
|
|
1086
|
+
const lastCall = global.fetch.mock.calls[global.fetch.mock.calls.length - 1];
|
|
1087
|
+
expect(lastCall[0]).toContain('testCustomers');
|
|
1088
|
+
expect(lastCall[1].method).toBe('POST');
|
|
1089
|
+
expect(lastCall[1].body).toBe(JSON.stringify(payload));
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
it('should call fetch with /testCustomers URL and POST method', () => {
|
|
1093
|
+
const payload = { customer: { email: 'n@b.co', firstName: 'N' } };
|
|
1094
|
+
createTestCustomer(payload);
|
|
1095
|
+
expect(global.fetch).toHaveBeenCalled();
|
|
1096
|
+
const lastCall = global.fetch.mock.calls[global.fetch.mock.calls.length - 1];
|
|
1097
|
+
expect(lastCall[0]).toContain('testCustomers');
|
|
1098
|
+
expect(lastCall[1].method).toBe('POST');
|
|
1099
|
+
expect(lastCall[1].body).toBe(JSON.stringify(payload));
|
|
1100
|
+
});
|
|
1101
|
+
});
|
package/utils/commonUtils.js
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
} from "../v2Containers/CreativesContainer/constants";
|
|
18
18
|
import { GLOBAL_CONVERT_OPTIONS } from "../v2Components/FormBuilder/constants";
|
|
19
19
|
import { SMS_TRAI_VAR } from '../v2Containers/SmsTrai/Edit/constants';
|
|
20
|
+
import { EMAIL_REGEX, PHONE_REGEX } from '../v2Components/CommonTestAndPreview/constants';
|
|
21
|
+
|
|
20
22
|
export const apiMessageFormatHandler = (id, fallback) => (
|
|
21
23
|
<FormattedMessage id={id} defaultMessage={fallback} />
|
|
22
24
|
);
|
|
@@ -530,3 +532,29 @@ export const checkForPersonalizationTokens = (formData) => {
|
|
|
530
532
|
}
|
|
531
533
|
return false;
|
|
532
534
|
};
|
|
535
|
+
|
|
536
|
+
export const isValidEmail = (email) => EMAIL_REGEX.test(email);
|
|
537
|
+
export const isValidMobile = (mobile) => PHONE_REGEX.test(mobile);
|
|
538
|
+
|
|
539
|
+
export const formatPhoneNumber = (phone) => {
|
|
540
|
+
if (!phone) return '';
|
|
541
|
+
return String(phone).replace(/[^\d]/g, '');
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* TRAI sender IDs on persisted RCS SMS fallback: may live on the merged object, under
|
|
546
|
+
* `templateConfigs`, or (legacy) `templateConfigs.header`. Same resolution order as
|
|
547
|
+
* `createPayload` in `Rcs/index.js`.
|
|
548
|
+
*/
|
|
549
|
+
export function extractRegisteredSenderIdsFromSmsFallbackRecord(record) {
|
|
550
|
+
if (!record || typeof record !== 'object') return null;
|
|
551
|
+
const tc = record.templateConfigs && typeof record.templateConfigs === 'object'
|
|
552
|
+
? record.templateConfigs
|
|
553
|
+
: {};
|
|
554
|
+
const candidates = [record.registeredSenderIds, tc.registeredSenderIds, tc.header];
|
|
555
|
+
for (let i = 0; i < candidates.length; i += 1) {
|
|
556
|
+
const a = candidates[i];
|
|
557
|
+
if (Array.isArray(a) && a.length > 0) return a;
|
|
558
|
+
}
|
|
559
|
+
return null;
|
|
560
|
+
}
|
package/utils/tagValidations.js
CHANGED
|
@@ -207,8 +207,7 @@ export const validateIfTagClosed = (value) => {
|
|
|
207
207
|
export const preprocessHtml = (content) => {
|
|
208
208
|
const replacements = {
|
|
209
209
|
"'": "'",
|
|
210
|
-
""": "'
|
|
211
|
-
'"': "'",
|
|
210
|
+
""": '"',
|
|
212
211
|
"&": "&",
|
|
213
212
|
"<": "<",
|
|
214
213
|
">": ">",
|
|
@@ -226,7 +225,7 @@ export const preprocessHtml = (content) => {
|
|
|
226
225
|
});
|
|
227
226
|
|
|
228
227
|
// Step 2: Perform the standard replacements on the entire content
|
|
229
|
-
return contentWithStyleFixes?.replace(/'|"|&|<|>
|
|
228
|
+
return contentWithStyleFixes?.replace(/'|"|&|<|>|\n/g, (match) => replacements[match]);
|
|
230
229
|
};
|
|
231
230
|
|
|
232
231
|
//this is used to get the subtags from custom or extended tags
|
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
COMBINED_SMS_TEMPLATE_VAR_REGEX,
|
|
8
8
|
DEFAULT_MUSTACHE_VAR_REGEX,
|
|
9
9
|
DLT_HASH_VAR_TOKEN_FULL_STRING_REGEX,
|
|
10
|
+
DLT_HASH_TOKEN_INNER_NAME_REGEX,
|
|
10
11
|
DLT_VAR_BODY_CAPTURE_REGEX,
|
|
12
|
+
MUSTACHE_TOKEN_INNER_NAME_REGEX,
|
|
11
13
|
MUSTACHE_VAR_NAME_CAPTURE_REGEX,
|
|
12
14
|
MUSTACHE_VAR_TOKEN_FULL_STRING_REGEX,
|
|
13
15
|
} from '../constants/unified';
|
|
@@ -115,13 +117,36 @@ export const extractTemplateVariables = (templateStr = '', captureRegex) => {
|
|
|
115
117
|
return variables;
|
|
116
118
|
};
|
|
117
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Looks up the inner name of a `{{name}}` or `{#name#}` token in a flat key→value map.
|
|
122
|
+
* Handles both exact matches and dot-path suffixes (e.g. `tag.FORMAT_1` → name `FORMAT_1`).
|
|
123
|
+
* Returns the resolved string value, or `undefined` if not found / blank.
|
|
124
|
+
*/
|
|
125
|
+
function resolveUserVarForToken(segment, userVarMap) {
|
|
126
|
+
if (!userVarMap || typeof userVarMap !== 'object') return undefined;
|
|
127
|
+
const mMustache = segment.match(MUSTACHE_TOKEN_INNER_NAME_REGEX);
|
|
128
|
+
const mDlt = segment.match(DLT_HASH_TOKEN_INNER_NAME_REGEX);
|
|
129
|
+
const innerName = ((mMustache ? mMustache[1] : (mDlt ? mDlt[1] : '')) ?? '').trim();
|
|
130
|
+
if (!innerName) return undefined;
|
|
131
|
+
const direct = userVarMap[innerName];
|
|
132
|
+
if (direct != null && String(direct).trim() !== '') return String(direct);
|
|
133
|
+
const hit = Object.keys(userVarMap).find((k) => k === innerName || k.endsWith(`.${innerName}`));
|
|
134
|
+
if (hit != null && userVarMap[hit] != null && String(userVarMap[hit]).trim() !== '') return String(userVarMap[hit]);
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
118
138
|
/**
|
|
119
139
|
* SMS / DLT template preview: replace `{{…}}` / `{#…#}` tokens using `varMapData` keys `${token}_${index}`.
|
|
120
140
|
* Used by SmsTraiEdit (RCS SMS fallback) and UnifiedPreview fallback SMS bubble.
|
|
121
141
|
* DLT `{#…#}`: empty / unset slot values show the raw token (matches DLT preview UX); mustache `{{…}}` still
|
|
122
142
|
* resolves to empty when the slot is cleared.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} templateStr
|
|
145
|
+
* @param {Object} varMapData - Slot map (`${token}_${index}` → value)
|
|
146
|
+
* @param {Object|null} [userVarMap] - Optional Test & Preview custom values; used as fallback when a slot
|
|
147
|
+
* is absent or blank. When omitted the function behaves identically to its original two-argument form.
|
|
123
148
|
*/
|
|
124
|
-
export const getFallbackResolvedContent = (templateStr = '', varMapData = {}) => {
|
|
149
|
+
export const getFallbackResolvedContent = (templateStr = '', varMapData = {}, userVarMap = null) => {
|
|
125
150
|
const fallbackVarSlotMap = varMapData ?? {};
|
|
126
151
|
const templateSegments = splitTemplateVarString(templateStr, COMBINED_SMS_TEMPLATE_VAR_REGEX);
|
|
127
152
|
return templateSegments
|
|
@@ -132,13 +157,17 @@ export const getFallbackResolvedContent = (templateStr = '', varMapData = {}) =>
|
|
|
132
157
|
if (Object.prototype.hasOwnProperty.call(fallbackVarSlotMap, slotKey)) {
|
|
133
158
|
const slotValue = fallbackVarSlotMap[slotKey];
|
|
134
159
|
if (isDltHashVarToken(segment)) {
|
|
135
|
-
if (slotValue == null)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
160
|
+
if (slotValue == null || String(slotValue).trim() === '') {
|
|
161
|
+
if (userVarMap) { const uv = resolveUserVarForToken(segment, userVarMap); if (uv !== undefined) return uv; }
|
|
162
|
+
return segment;
|
|
163
|
+
}
|
|
164
|
+
return String(slotValue);
|
|
139
165
|
}
|
|
140
|
-
|
|
166
|
+
if (slotValue != null && String(slotValue).trim() !== '') return String(slotValue);
|
|
167
|
+
if (userVarMap) { const uv = resolveUserVarForToken(segment, userVarMap); if (uv !== undefined) return uv; }
|
|
168
|
+
return '';
|
|
141
169
|
}
|
|
170
|
+
if (userVarMap) { const uv = resolveUserVarForToken(segment, userVarMap); if (uv !== undefined) return uv; }
|
|
142
171
|
if (isDltHashVarToken(segment)) return segment;
|
|
143
172
|
return '';
|
|
144
173
|
})
|
|
@@ -8,6 +8,11 @@ import {
|
|
|
8
8
|
validateCarouselCards,
|
|
9
9
|
hasPersonalizationTags,
|
|
10
10
|
checkForPersonalizationTokens,
|
|
11
|
+
isValidEmail,
|
|
12
|
+
isValidMobile,
|
|
13
|
+
formatPhoneNumber,
|
|
14
|
+
getMessageForDevice,
|
|
15
|
+
getTitleForDevice,
|
|
11
16
|
} from "../commonUtils";
|
|
12
17
|
import { skipTags } from "../tagValidations";
|
|
13
18
|
import { SMS_TRAI_VAR } from '../../v2Containers/SmsTrai/Edit/constants';
|
|
@@ -629,6 +634,170 @@ describe("validateLiquidTemplateContent", () => {
|
|
|
629
634
|
});
|
|
630
635
|
});
|
|
631
636
|
|
|
637
|
+
describe("isValidEmail", () => {
|
|
638
|
+
it("returns true for valid email addresses", () => {
|
|
639
|
+
expect(isValidEmail("user@example.com")).toBe(true);
|
|
640
|
+
expect(isValidEmail("test.user@domain.co")).toBe(true);
|
|
641
|
+
expect(isValidEmail("a@b.co")).toBe(true);
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it("returns false for invalid email addresses", () => {
|
|
645
|
+
expect(isValidEmail("")).toBe(false);
|
|
646
|
+
expect(isValidEmail("invalid")).toBe(false);
|
|
647
|
+
expect(isValidEmail("@nodomain.com")).toBe(false);
|
|
648
|
+
expect(isValidEmail("noatsign.com")).toBe(false);
|
|
649
|
+
expect(isValidEmail("missingtld@domain")).toBe(false);
|
|
650
|
+
expect(isValidEmail(" user@example.com ")).toBe(false);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it("returns true for edge case single-char local part", () => {
|
|
654
|
+
expect(isValidEmail("x@y.co")).toBe(true);
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
describe("isValidMobile", () => {
|
|
659
|
+
it("returns true for valid mobile numbers (8-15 digits, no leading zero)", () => {
|
|
660
|
+
expect(isValidMobile("12345678")).toBe(true);
|
|
661
|
+
expect(isValidMobile("9123456789")).toBe(true);
|
|
662
|
+
expect(isValidMobile("123456789012345")).toBe(true);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it("returns false for invalid mobile numbers", () => {
|
|
666
|
+
expect(isValidMobile("")).toBe(false);
|
|
667
|
+
expect(isValidMobile("01234567")).toBe(false);
|
|
668
|
+
expect(isValidMobile("1234567")).toBe(false);
|
|
669
|
+
expect(isValidMobile("1234567890123456")).toBe(false);
|
|
670
|
+
expect(isValidMobile("abc12345678")).toBe(false);
|
|
671
|
+
expect(isValidMobile("12345 67890")).toBe(false);
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it("returns true for exactly 8 and exactly 15 digits", () => {
|
|
675
|
+
expect(isValidMobile("12345678")).toBe(true);
|
|
676
|
+
expect(isValidMobile("123456789012345")).toBe(true);
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
describe("formatPhoneNumber", () => {
|
|
681
|
+
it("returns empty string for falsy input", () => {
|
|
682
|
+
expect(formatPhoneNumber("")).toBe("");
|
|
683
|
+
expect(formatPhoneNumber(null)).toBe("");
|
|
684
|
+
expect(formatPhoneNumber(undefined)).toBe("");
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it("strips non-digit characters", () => {
|
|
688
|
+
expect(formatPhoneNumber("91 234 567 890")).toBe("91234567890");
|
|
689
|
+
expect(formatPhoneNumber("+91-234567890")).toBe("91234567890");
|
|
690
|
+
expect(formatPhoneNumber("(123) 456-7890")).toBe("1234567890");
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
it("returns digits-only string unchanged", () => {
|
|
694
|
+
expect(formatPhoneNumber("9123456789")).toBe("9123456789");
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it("returns empty string for whitespace-only input", () => {
|
|
698
|
+
expect(formatPhoneNumber(" ")).toBe("");
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
describe("hasPersonalizationTags", () => {
|
|
703
|
+
it("returns true when text has liquid tags {{ }}", () => {
|
|
704
|
+
expect(hasPersonalizationTags("Hello {{name}}")).toBe(true);
|
|
705
|
+
expect(hasPersonalizationTags("{{foo}}")).toBe(true);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it("returns true when text has bracket tags [ ]", () => {
|
|
709
|
+
expect(hasPersonalizationTags("Hello [event.name]")).toBe(true);
|
|
710
|
+
expect(hasPersonalizationTags("[tag]")).toBe(true);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it("returns false for empty or no tags", () => {
|
|
714
|
+
expect(hasPersonalizationTags("")).toBeFalsy();
|
|
715
|
+
expect(hasPersonalizationTags()).toBeFalsy();
|
|
716
|
+
expect(hasPersonalizationTags("plain text")).toBe(false);
|
|
717
|
+
expect(hasPersonalizationTags("only {{ open")).toBe(false);
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
it("returns false when only [ or only ] present without matching pair", () => {
|
|
721
|
+
expect(hasPersonalizationTags("only [ open")).toBe(false);
|
|
722
|
+
expect(hasPersonalizationTags("only ] close")).toBe(false);
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it("returns true when liquid tags have content with spaces", () => {
|
|
726
|
+
expect(hasPersonalizationTags("Hello {{ customer.name }}")).toBe(true);
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
describe("getMessageForDevice", () => {
|
|
731
|
+
it("returns message for device from templateData", () => {
|
|
732
|
+
const templateData = {
|
|
733
|
+
versions: {
|
|
734
|
+
android: { base: { expandableDetails: { message: "Android msg" } } },
|
|
735
|
+
ios: { base: { expandableDetails: { message: "iOS msg" } } },
|
|
736
|
+
},
|
|
737
|
+
};
|
|
738
|
+
expect(getMessageForDevice(templateData, "android")).toBe("Android msg");
|
|
739
|
+
expect(getMessageForDevice(templateData, "ios")).toBe("iOS msg");
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
it("returns undefined for missing path", () => {
|
|
743
|
+
expect(getMessageForDevice(null, "android")).toBeUndefined();
|
|
744
|
+
expect(getMessageForDevice({}, "android")).toBeUndefined();
|
|
745
|
+
expect(getMessageForDevice({ versions: {} }, "android")).toBeUndefined();
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
it("returns undefined when base exists but expandableDetails is missing", () => {
|
|
749
|
+
expect(getMessageForDevice({ versions: { android: { base: {} } } }, "android")).toBeUndefined();
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
describe("getTitleForDevice", () => {
|
|
754
|
+
it("returns title for device from templateData", () => {
|
|
755
|
+
const templateData = {
|
|
756
|
+
versions: {
|
|
757
|
+
android: { base: { title: "Android title" } },
|
|
758
|
+
ios: { base: { title: "iOS title" } },
|
|
759
|
+
},
|
|
760
|
+
};
|
|
761
|
+
expect(getTitleForDevice(templateData, "android")).toBe("Android title");
|
|
762
|
+
expect(getTitleForDevice(templateData, "ios")).toBe("iOS title");
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it("returns empty string for missing title", () => {
|
|
766
|
+
expect(getTitleForDevice(null, "android")).toBe("");
|
|
767
|
+
expect(getTitleForDevice({}, "android")).toBe("");
|
|
768
|
+
expect(getTitleForDevice({ versions: { android: { base: {} } } }, "android")).toBe("");
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
it("returns empty string when base.title is undefined", () => {
|
|
772
|
+
expect(getTitleForDevice({ versions: { android: { base: { title: undefined } } } }, "android")).toBe("");
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
describe("checkForPersonalizationTokens", () => {
|
|
777
|
+
it("returns true when formData contains liquid or bracket tokens", () => {
|
|
778
|
+
expect(checkForPersonalizationTokens({ 0: { content: "Hi {{name}}" } })).toBe(true);
|
|
779
|
+
expect(checkForPersonalizationTokens({ tab1: { message: "Hello [event.id]" } })).toBe(true);
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
it("returns false for empty or no tokens", () => {
|
|
783
|
+
expect(checkForPersonalizationTokens(null)).toBe(false);
|
|
784
|
+
expect(checkForPersonalizationTokens({})).toBe(false);
|
|
785
|
+
expect(checkForPersonalizationTokens({ 0: { content: "plain" } })).toBe(false);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it("returns false for non-object formData", () => {
|
|
789
|
+
expect(checkForPersonalizationTokens("string")).toBe(false);
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
it("returns true for two-level nested object containing token", () => {
|
|
793
|
+
expect(checkForPersonalizationTokens({ tab: { body: "Hi {{name}}" } })).toBe(true);
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
it("returns false for formData with array value (no string tokens)", () => {
|
|
797
|
+
expect(checkForPersonalizationTokens({ 0: { items: ["a", "b"] } })).toBe(false);
|
|
798
|
+
});
|
|
799
|
+
});
|
|
800
|
+
|
|
632
801
|
describe("validateMobilePushContent", () => {
|
|
633
802
|
const formatMessage = jest.fn(msg => msg.id);
|
|
634
803
|
const messages = {
|
|
@@ -642,7 +642,7 @@ describe('preprocessHtml', () => {
|
|
|
642
642
|
|
|
643
643
|
it('should replace " with double quotes', () => {
|
|
644
644
|
const input = 'She said, "Hello, World!"';
|
|
645
|
-
const expectedOutput =
|
|
645
|
+
const expectedOutput = 'She said, "Hello, World!"';
|
|
646
646
|
expect(preprocessHtml(input)).toEqual(expectedOutput);
|
|
647
647
|
});
|
|
648
648
|
|
|
@@ -157,4 +157,48 @@ describe('templateVarUtils', () => {
|
|
|
157
157
|
expect(extractTemplateVariables('{{x}} {{y}}', globalRe)).toEqual(['x', 'y']);
|
|
158
158
|
});
|
|
159
159
|
});
|
|
160
|
+
|
|
161
|
+
describe('getFallbackResolvedContent — userVarMap (third param)', () => {
|
|
162
|
+
it('falls back to userVarMap for mustache slot absent from varMapData', () => {
|
|
163
|
+
expect(getFallbackResolvedContent('Hi {{name}}', {}, { name: 'Alice' })).toBe('Hi Alice');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('falls back to userVarMap for DLT slot absent from varMapData', () => {
|
|
167
|
+
expect(getFallbackResolvedContent('{#city#}', {}, { city: 'Mumbai' })).toBe('Mumbai');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('falls back to userVarMap when mustache slot is present but blank', () => {
|
|
171
|
+
expect(getFallbackResolvedContent('{{x}}', { '{{x}}_0': '' }, { x: 'val' })).toBe('val');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('falls back to userVarMap when DLT slot is present but blank', () => {
|
|
175
|
+
expect(getFallbackResolvedContent('{#x#}', { '{#x#}_0': '' }, { x: 'val' })).toBe('val');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('slot wins over userVarMap when mustache slot is non-empty', () => {
|
|
179
|
+
expect(getFallbackResolvedContent('{{x}}', { '{{x}}_0': 'slot-val' }, { x: 'user-val' })).toBe('slot-val');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('slot wins over userVarMap when DLT slot is non-empty', () => {
|
|
183
|
+
expect(getFallbackResolvedContent('{#x#}', { '{#x#}_0': 'slot' }, { x: 'user' })).toBe('slot');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('resolves dot-path suffix key from userVarMap', () => {
|
|
187
|
+
expect(
|
|
188
|
+
getFallbackResolvedContent('{{FORMAT_1}}', {}, { 'expiry.FORMAT_1': 'Dec 2025' }),
|
|
189
|
+
).toBe('Dec 2025');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('userVarMap null behaves identically to the two-argument form', () => {
|
|
193
|
+
expect(getFallbackResolvedContent('{{a}}', { '{{a}}_0': 'x' }, null)).toBe('x');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('blank userVarMap value leaves mustache token as empty string', () => {
|
|
197
|
+
expect(getFallbackResolvedContent('{{a}}', {}, { a: '' })).toBe('');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('blank userVarMap value leaves DLT token as raw token', () => {
|
|
201
|
+
expect(getFallbackResolvedContent('{#a#}', {}, { a: '' })).toBe('{#a#}');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
160
204
|
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import PropTypes from "prop-types";
|
|
2
|
+
import { FormattedMessage } from "react-intl";
|
|
3
|
+
import CapButton from "@capillarytech/cap-ui-library/CapButton";
|
|
4
|
+
import CapIcon from "@capillarytech/cap-ui-library/CapIcon";
|
|
5
|
+
import messages from "./messages";
|
|
6
|
+
import React from "react";
|
|
7
|
+
|
|
8
|
+
const AddTestCustomerButton = ({
|
|
9
|
+
searchValue,
|
|
10
|
+
handleAddTestCustomer
|
|
11
|
+
}) => (
|
|
12
|
+
<CapButton
|
|
13
|
+
onClick={handleAddTestCustomer}
|
|
14
|
+
type="flat"
|
|
15
|
+
size="small"
|
|
16
|
+
className="test-customer-add-btn"
|
|
17
|
+
prefix={
|
|
18
|
+
<CapIcon
|
|
19
|
+
type="add-profile"
|
|
20
|
+
className="add-test-customer-icon"
|
|
21
|
+
/>
|
|
22
|
+
}
|
|
23
|
+
>
|
|
24
|
+
<FormattedMessage
|
|
25
|
+
{...messages.addTestCustomerWithValue}
|
|
26
|
+
values={{
|
|
27
|
+
searchValue: (
|
|
28
|
+
<span className="test-customer-add-btn-value" title={searchValue || ""}>
|
|
29
|
+
"{searchValue}"
|
|
30
|
+
</span>
|
|
31
|
+
),
|
|
32
|
+
}}
|
|
33
|
+
/>
|
|
34
|
+
</CapButton>
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
AddTestCustomerButton.propTypes = {
|
|
38
|
+
searchValue: PropTypes.string.isRequired,
|
|
39
|
+
handleAddTestCustomer: PropTypes.func.isRequired
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default AddTestCustomerButton;
|