@capillarytech/creatives-library 9.0.50-alpha.1 → 9.0.50-alpha.2
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/package.json +1 -1
- package/v2Components/CommonTestAndPreview/UnifiedPreview/ViberPreviewContent.js +15 -106
- package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +1 -139
- package/v2Components/CommonTestAndPreview/constants.js +0 -2
- package/v2Components/CommonTestAndPreview/index.js +26 -240
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/ViberPreviewContent.test.js +0 -364
- package/v2Components/CommonTestAndPreview/tests/utils.test.js +0 -49
- package/v2Components/CommonTestAndPreview/utils.js +0 -20
- package/v2Containers/CreativesContainer/index.js +4 -9
- package/v2Containers/CreativesContainer/tests/index.test.js +4 -3
- package/v2Containers/Rcs/carouselUtils.js +43 -13
- package/v2Containers/Rcs/index.js +32 -8
- package/v2Containers/Rcs/tests/carouselUtils.test.js +23 -4
- package/v2Containers/Rcs/tests/index.test.js +265 -3
- package/v2Containers/Rcs/tests/utils.test.js +59 -74
- package/v2Containers/Rcs/utils.js +9 -34
- package/v2Containers/Templates/_templates.scss +1 -179
- package/v2Containers/Templates/index.js +10 -97
- package/v2Containers/Viber/constants.js +0 -21
- package/v2Containers/Viber/index.js +43 -711
- package/v2Containers/Viber/index.scss +0 -175
- package/v2Containers/Viber/messages.js +0 -121
- package/v2Containers/Viber/tests/index.test.js +0 -80
- package/v2Components/CommonTestAndPreview/UnifiedPreview/ViberCarouselPreviewCards.js +0 -127
- package/v2Components/CommonTestAndPreview/UnifiedPreview/_viberCarouselPreviewCards.scss +0 -133
|
@@ -853,7 +853,7 @@ describe('buildCarouselCardContentForPayload', () => {
|
|
|
853
853
|
expect(out[0].cardVarMapped).toBeUndefined();
|
|
854
854
|
});
|
|
855
855
|
|
|
856
|
-
it('
|
|
856
|
+
it('renumbers non-numeric title/description tokens to sequential {{N}} slots and keys cardVarMapped by number', () => {
|
|
857
857
|
const out = buildCarouselCardContentForPayload(
|
|
858
858
|
[{ title: 'Hi {{name}}', description: 'Pts {{points}}', mediaType: RCS_MEDIA_TYPES.NONE }],
|
|
859
859
|
{
|
|
@@ -863,7 +863,9 @@ describe('buildCarouselCardContentForPayload', () => {
|
|
|
863
863
|
rcsVarRegex,
|
|
864
864
|
},
|
|
865
865
|
);
|
|
866
|
-
expect(out[0].
|
|
866
|
+
expect(out[0].title).toBe('Hi {{1}}');
|
|
867
|
+
expect(out[0].description).toBe('Pts {{2}}');
|
|
868
|
+
expect(out[0].cardVarMapped).toEqual({ 1: 'Bob', 2: '' });
|
|
867
869
|
});
|
|
868
870
|
|
|
869
871
|
it('defaults an unmapped token value to empty string via sanitizeCardVarMappedValue', () => {
|
|
@@ -871,7 +873,8 @@ describe('buildCarouselCardContentForPayload', () => {
|
|
|
871
873
|
[{ title: 'Hi {{missing}}', description: 'D', mediaType: RCS_MEDIA_TYPES.NONE }],
|
|
872
874
|
{ isSlotMappingMode: true, cardVarMapped: {}, selectedCarouselHeight: 'SHORT', rcsVarRegex },
|
|
873
875
|
);
|
|
874
|
-
expect(out[0].
|
|
876
|
+
expect(out[0].title).toBe('Hi {{1}}');
|
|
877
|
+
expect(out[0].cardVarMapped).toEqual({ 1: '' });
|
|
875
878
|
});
|
|
876
879
|
|
|
877
880
|
it('handles a missing cardVarMapped object entirely', () => {
|
|
@@ -879,7 +882,23 @@ describe('buildCarouselCardContentForPayload', () => {
|
|
|
879
882
|
[{ title: 'Hi {{name}}', description: 'D', mediaType: RCS_MEDIA_TYPES.NONE }],
|
|
880
883
|
{ isSlotMappingMode: true, cardVarMapped: undefined, selectedCarouselHeight: 'SHORT', rcsVarRegex },
|
|
881
884
|
);
|
|
882
|
-
expect(out[0].
|
|
885
|
+
expect(out[0].title).toBe('Hi {{1}}');
|
|
886
|
+
expect(out[0].cardVarMapped).toEqual({ 1: '' });
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
it('leaves an already-numeric slot untouched and only renumbers the legacy/named slot alongside it', () => {
|
|
890
|
+
const out = buildCarouselCardContentForPayload(
|
|
891
|
+
[{ title: 'Hi {{1}}', description: 'Pts {{first_name}} exp {{3}}', mediaType: RCS_MEDIA_TYPES.NONE }],
|
|
892
|
+
{
|
|
893
|
+
isSlotMappingMode: true,
|
|
894
|
+
cardVarMapped: { 1: 'test', first_name: '{{first_name}}', 3: 'test' },
|
|
895
|
+
selectedCarouselHeight: 'SHORT',
|
|
896
|
+
rcsVarRegex,
|
|
897
|
+
},
|
|
898
|
+
);
|
|
899
|
+
expect(out[0].title).toBe('Hi {{1}}');
|
|
900
|
+
expect(out[0].description).toBe('Pts {{2}} exp {{3}}');
|
|
901
|
+
expect(out[0].cardVarMapped).toEqual({ 1: 'test', 2: '{{first_name}}', 3: 'test' });
|
|
883
902
|
});
|
|
884
903
|
|
|
885
904
|
it('skips a matched token that strips down to an empty variable name', () => {
|
|
@@ -1259,11 +1259,14 @@ describe('RCS createPayload', () => {
|
|
|
1259
1259
|
});
|
|
1260
1260
|
wrapper.update();
|
|
1261
1261
|
|
|
1262
|
-
// The
|
|
1263
|
-
//
|
|
1262
|
+
// The persisted API contract requires title/description to only ever hold {{1}}, {{2}}, …
|
|
1263
|
+
// — never the picked tag's name — so the focused occurrence's own token is normalized to its
|
|
1264
|
+
// numeric slot instead of being replaced with {{first_name}}; the tag itself lives in
|
|
1265
|
+
// cardVarMapped. The untouched duplicate keeps its original {{user_name}} token but syncs to
|
|
1266
|
+
// the same resolved value.
|
|
1264
1267
|
const renamedVarArea = wrapper.find('CapInputTextArea').filterWhere((n) => {
|
|
1265
1268
|
const id = n.prop('id') || '';
|
|
1266
|
-
return id.includes('{{
|
|
1269
|
+
return id.includes('{{1}}_');
|
|
1267
1270
|
});
|
|
1268
1271
|
const duplicateVarArea = wrapper.find('CapInputTextArea').filterWhere((n) => {
|
|
1269
1272
|
const id = n.prop('id') || '';
|
|
@@ -1273,6 +1276,265 @@ describe('RCS createPayload', () => {
|
|
|
1273
1276
|
expect(duplicateVarArea.at(0).prop('value')).toBe('{{first_name}}');
|
|
1274
1277
|
});
|
|
1275
1278
|
|
|
1279
|
+
it('normalizes a hydrated legacy/semantic token to its numeric slot in the payload even without touching TagList', () => {
|
|
1280
|
+
const getFormData = jest.fn();
|
|
1281
|
+
const templateData = {
|
|
1282
|
+
name: 'RenewalReminder',
|
|
1283
|
+
versions: {
|
|
1284
|
+
base: {
|
|
1285
|
+
content: {
|
|
1286
|
+
RCS: {
|
|
1287
|
+
rcsContent: {
|
|
1288
|
+
cardType: 'STANDALONE',
|
|
1289
|
+
cardSettings: { cardOrientation: 'VERTICAL', cardWidth: 'SMALL' },
|
|
1290
|
+
cardContent: [
|
|
1291
|
+
{
|
|
1292
|
+
title: '',
|
|
1293
|
+
description:
|
|
1294
|
+
'Hello {{1}}, your {{first_name}} renews on {{3}}. Renew via our website to avoid disruption. Thank you, Capillary Technologies.',
|
|
1295
|
+
mediaType: 'NONE',
|
|
1296
|
+
// Slots 1 and 3 were typed directly; slot 2's token was hydrated as a legacy
|
|
1297
|
+
// semantic name that was never touched via TagList (self-referential value).
|
|
1298
|
+
cardVarMapped: { 1: 'test', first_name: '{{first_name}}', 3: 'test' },
|
|
1299
|
+
suggestions: [
|
|
1300
|
+
{ type: 'QUICK_REPLY', text: 'stop' },
|
|
1301
|
+
],
|
|
1302
|
+
},
|
|
1303
|
+
],
|
|
1304
|
+
contentType: 'RICHCARD',
|
|
1305
|
+
},
|
|
1306
|
+
},
|
|
1307
|
+
},
|
|
1308
|
+
},
|
|
1309
|
+
},
|
|
1310
|
+
type: 'RCS',
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
const wrapper = mountWithIntl(
|
|
1314
|
+
<Provider store={store}>
|
|
1315
|
+
<Rcs
|
|
1316
|
+
actions={{
|
|
1317
|
+
clearCreateResponse: jest.fn(),
|
|
1318
|
+
getTemplateDetails: jest.fn(),
|
|
1319
|
+
uploadRcsAsset: jest.fn(),
|
|
1320
|
+
clearRcsMediaAsset: jest.fn(),
|
|
1321
|
+
editTemplate: jest.fn(),
|
|
1322
|
+
clearEditResponse: jest.fn(),
|
|
1323
|
+
}}
|
|
1324
|
+
globalActions={{ fetchSchemaForEntity }}
|
|
1325
|
+
onCreateComplete={onCreateComplete}
|
|
1326
|
+
handleClose={handleClose}
|
|
1327
|
+
intl={{ formatMessage }}
|
|
1328
|
+
location={{ pathname: '/rcs/create', query: { type: false, module: 'default' }, search: '' }}
|
|
1329
|
+
params={params}
|
|
1330
|
+
templateData={templateData}
|
|
1331
|
+
rcsData={{}}
|
|
1332
|
+
isFullMode={false}
|
|
1333
|
+
isEditFlow={false}
|
|
1334
|
+
loadingTags={false}
|
|
1335
|
+
metaEntities={[]}
|
|
1336
|
+
isDltEnabled={false}
|
|
1337
|
+
smsRegister={'DLT'}
|
|
1338
|
+
getFormData={getFormData}
|
|
1339
|
+
/>
|
|
1340
|
+
</Provider>,
|
|
1341
|
+
);
|
|
1342
|
+
|
|
1343
|
+
// No TagList/VarSegment interaction at all — click Done straight away.
|
|
1344
|
+
const doneBtn = wrapper.find('CapButton.rcs-done-btn').at(0);
|
|
1345
|
+
expect(doneBtn.exists()).toBe(true);
|
|
1346
|
+
act(() => {
|
|
1347
|
+
doneBtn.prop('onClick')();
|
|
1348
|
+
});
|
|
1349
|
+
expect(getFormData).toHaveBeenCalled();
|
|
1350
|
+
const payloadArg = getFormData.mock.calls[0][0].value;
|
|
1351
|
+
const card = payloadArg.versions.base.content.RCS.rcsContent.cardContent[0];
|
|
1352
|
+
|
|
1353
|
+
expect(card.description).toBe(
|
|
1354
|
+
'Hello {{1}}, your {{2}} renews on {{3}}. Renew via our website to avoid disruption. Thank you, Capillary Technologies.',
|
|
1355
|
+
);
|
|
1356
|
+
expect(card.cardVarMapped).toEqual({ 1: 'test', 2: '{{first_name}}', 3: 'test' });
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
it('standalone full-mode EDIT of an already-approved template: correct sequential numeric slots pass through byte-identical (no regression)', async () => {
|
|
1360
|
+
const getFormData = jest.fn();
|
|
1361
|
+
const templateData = {
|
|
1362
|
+
name: 'ApprovedPromoCard',
|
|
1363
|
+
versions: {
|
|
1364
|
+
base: {
|
|
1365
|
+
content: {
|
|
1366
|
+
RCS: {
|
|
1367
|
+
rcsContent: {
|
|
1368
|
+
cardType: 'STANDALONE',
|
|
1369
|
+
cardSettings: { cardOrientation: 'VERTICAL', cardWidth: 'SMALL' },
|
|
1370
|
+
cardContent: [
|
|
1371
|
+
{
|
|
1372
|
+
title: 'Hi {{1}}',
|
|
1373
|
+
description: 'Your order {{2}} ships on {{3}}.',
|
|
1374
|
+
mediaType: 'IMAGE',
|
|
1375
|
+
media: { mediaUrl: 'https://cdn.example.com/card.png' },
|
|
1376
|
+
cardVarMapped: { 1: '{{first_name}}', 2: 'ORD-42', 3: '{{ship_date}}' },
|
|
1377
|
+
suggestions: [{ type: 'QUICK_REPLY', text: 'stop' }],
|
|
1378
|
+
},
|
|
1379
|
+
],
|
|
1380
|
+
contentType: 'RICHCARD',
|
|
1381
|
+
},
|
|
1382
|
+
},
|
|
1383
|
+
},
|
|
1384
|
+
},
|
|
1385
|
+
},
|
|
1386
|
+
type: 'RCS',
|
|
1387
|
+
};
|
|
1388
|
+
|
|
1389
|
+
const rcsProps = (rcsData) => ({
|
|
1390
|
+
actions: {
|
|
1391
|
+
clearCreateResponse: jest.fn(),
|
|
1392
|
+
getTemplateDetails: jest.fn(),
|
|
1393
|
+
uploadRcsAsset: jest.fn(),
|
|
1394
|
+
clearRcsMediaAsset: jest.fn(),
|
|
1395
|
+
editTemplate: jest.fn(),
|
|
1396
|
+
clearEditResponse: jest.fn(),
|
|
1397
|
+
},
|
|
1398
|
+
globalActions: { fetchSchemaForEntity },
|
|
1399
|
+
onCreateComplete,
|
|
1400
|
+
handleClose,
|
|
1401
|
+
intl: { formatMessage },
|
|
1402
|
+
location: { pathname: '/rcs/edit', query: { type: false, module: 'default' }, search: '' },
|
|
1403
|
+
params,
|
|
1404
|
+
rcsData,
|
|
1405
|
+
isFullMode: true,
|
|
1406
|
+
isEditFlow: false,
|
|
1407
|
+
loadingTags: false,
|
|
1408
|
+
metaEntities: [],
|
|
1409
|
+
isDltEnabled: false,
|
|
1410
|
+
smsRegister: 'DLT',
|
|
1411
|
+
getFormData,
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1414
|
+
const wrapper = mountWithIntl(
|
|
1415
|
+
<Provider store={store}>
|
|
1416
|
+
<Rcs {...rcsProps({})} />
|
|
1417
|
+
</Provider>,
|
|
1418
|
+
);
|
|
1419
|
+
|
|
1420
|
+
// Mirrors real usage: the component mounts before rcsData.templateDetails arrives (fetched via
|
|
1421
|
+
// getTemplateDetails), then the prop updates once the template loads — matching how
|
|
1422
|
+
// rcsHydrationDetails / setEditFlow(true) actually fire in the app. Updating rcsData in the
|
|
1423
|
+
// same synchronous pass as mount would run the hydration effect and the templateType-reset
|
|
1424
|
+
// effect together against stale pre-hydration closures, clobbering the hydrated title/desc.
|
|
1425
|
+
await act(async () => {
|
|
1426
|
+
await Promise.resolve();
|
|
1427
|
+
});
|
|
1428
|
+
wrapper.update();
|
|
1429
|
+
await act(async () => {
|
|
1430
|
+
wrapper.setProps({
|
|
1431
|
+
children: <Rcs {...rcsProps({ templateDetails: templateData })} />,
|
|
1432
|
+
});
|
|
1433
|
+
await Promise.resolve();
|
|
1434
|
+
});
|
|
1435
|
+
wrapper.update();
|
|
1436
|
+
|
|
1437
|
+
const doneBtn = wrapper.find('CapButton.rcs-done-btn').at(0);
|
|
1438
|
+
expect(doneBtn.exists()).toBe(true);
|
|
1439
|
+
act(() => {
|
|
1440
|
+
doneBtn.prop('onClick')();
|
|
1441
|
+
});
|
|
1442
|
+
expect(getFormData).toHaveBeenCalled();
|
|
1443
|
+
const card = getFormData.mock.calls[0][0].value.versions.base.content.RCS.rcsContent.cardContent[0];
|
|
1444
|
+
|
|
1445
|
+
expect(card.title).toBe('Hi {{1}}');
|
|
1446
|
+
expect(card.description).toBe('Your order {{2}} ships on {{3}}.');
|
|
1447
|
+
expect(card.cardVarMapped).toEqual({ 1: '{{first_name}}', 2: 'ORD-42', 3: '{{ship_date}}' });
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
it('standalone full-mode EDIT: a legacy/semantic token still self-heals to its numeric slot (same fix applies outside library mode)', async () => {
|
|
1451
|
+
const getFormData = jest.fn();
|
|
1452
|
+
const templateData = {
|
|
1453
|
+
name: 'LegacyPromoCard',
|
|
1454
|
+
versions: {
|
|
1455
|
+
base: {
|
|
1456
|
+
content: {
|
|
1457
|
+
RCS: {
|
|
1458
|
+
rcsContent: {
|
|
1459
|
+
cardType: 'STANDALONE',
|
|
1460
|
+
cardSettings: { cardOrientation: 'VERTICAL', cardWidth: 'SMALL' },
|
|
1461
|
+
cardContent: [
|
|
1462
|
+
{
|
|
1463
|
+
title: '',
|
|
1464
|
+
// Legacy key "first_name" holds a DIFFERENT tag ({{loyalty_points}}) as its value —
|
|
1465
|
+
// deliberately not self-referential: normalizeCardVarMapped (full-mode hydration
|
|
1466
|
+
// only) clears a legacy key whose value is the exact same {{tagName}} as the key
|
|
1467
|
+
// itself, treating that as "never actually filled." That heuristic is pre-existing
|
|
1468
|
+
// and orthogonal to this fix, so avoid colliding with it here.
|
|
1469
|
+
description: 'Hello {{1}}, your {{first_name}} renews on {{3}}.',
|
|
1470
|
+
mediaType: 'NONE',
|
|
1471
|
+
cardVarMapped: { 1: 'test', first_name: '{{loyalty_points}}', 3: 'test' },
|
|
1472
|
+
suggestions: [{ type: 'QUICK_REPLY', text: 'stop' }],
|
|
1473
|
+
},
|
|
1474
|
+
],
|
|
1475
|
+
contentType: 'RICHCARD',
|
|
1476
|
+
},
|
|
1477
|
+
},
|
|
1478
|
+
},
|
|
1479
|
+
},
|
|
1480
|
+
},
|
|
1481
|
+
type: 'RCS',
|
|
1482
|
+
};
|
|
1483
|
+
|
|
1484
|
+
const rcsProps = (rcsData) => ({
|
|
1485
|
+
actions: {
|
|
1486
|
+
clearCreateResponse: jest.fn(),
|
|
1487
|
+
getTemplateDetails: jest.fn(),
|
|
1488
|
+
uploadRcsAsset: jest.fn(),
|
|
1489
|
+
clearRcsMediaAsset: jest.fn(),
|
|
1490
|
+
editTemplate: jest.fn(),
|
|
1491
|
+
clearEditResponse: jest.fn(),
|
|
1492
|
+
},
|
|
1493
|
+
globalActions: { fetchSchemaForEntity },
|
|
1494
|
+
onCreateComplete,
|
|
1495
|
+
handleClose,
|
|
1496
|
+
intl: { formatMessage },
|
|
1497
|
+
location: { pathname: '/rcs/edit', query: { type: false, module: 'default' }, search: '' },
|
|
1498
|
+
params,
|
|
1499
|
+
rcsData,
|
|
1500
|
+
isFullMode: true,
|
|
1501
|
+
isEditFlow: false,
|
|
1502
|
+
loadingTags: false,
|
|
1503
|
+
metaEntities: [],
|
|
1504
|
+
isDltEnabled: false,
|
|
1505
|
+
smsRegister: 'DLT',
|
|
1506
|
+
getFormData,
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
const wrapper = mountWithIntl(
|
|
1510
|
+
<Provider store={store}>
|
|
1511
|
+
<Rcs {...rcsProps({})} />
|
|
1512
|
+
</Provider>,
|
|
1513
|
+
);
|
|
1514
|
+
|
|
1515
|
+
// See the previous test for why hydration must arrive via a prop update, not the initial mount.
|
|
1516
|
+
await act(async () => {
|
|
1517
|
+
await Promise.resolve();
|
|
1518
|
+
});
|
|
1519
|
+
wrapper.update();
|
|
1520
|
+
await act(async () => {
|
|
1521
|
+
wrapper.setProps({
|
|
1522
|
+
children: <Rcs {...rcsProps({ templateDetails: templateData })} />,
|
|
1523
|
+
});
|
|
1524
|
+
await Promise.resolve();
|
|
1525
|
+
});
|
|
1526
|
+
wrapper.update();
|
|
1527
|
+
|
|
1528
|
+
const doneBtn = wrapper.find('CapButton.rcs-done-btn').at(0);
|
|
1529
|
+
act(() => {
|
|
1530
|
+
doneBtn.prop('onClick')();
|
|
1531
|
+
});
|
|
1532
|
+
const card = getFormData.mock.calls[0][0].value.versions.base.content.RCS.rcsContent.cardContent[0];
|
|
1533
|
+
|
|
1534
|
+
expect(card.description).toBe('Hello {{1}}, your {{2}} renews on {{3}}.');
|
|
1535
|
+
expect(card.cardVarMapped).toEqual({ 1: 'test', 2: '{{loyalty_points}}', 3: 'test' });
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1276
1538
|
it('should keep two tags + freetext inside the variable textarea in non-full mode edit (not merged into static text)', () => {
|
|
1277
1539
|
const templateData = {
|
|
1278
1540
|
name: 'TwoTagsAndFreeText',
|
|
@@ -604,77 +604,75 @@ describe('RCS utils', () => {
|
|
|
604
604
|
});
|
|
605
605
|
|
|
606
606
|
describe('mapRcsCardContentForConsumerWithResolvedTags', () => {
|
|
607
|
-
it('
|
|
608
|
-
const out = mapRcsCardContentForConsumerWithResolvedTags(
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
);
|
|
607
|
+
it('leaves numeric-slot title/description untouched — consumers get the same {{1}}, {{2}}, … contract as the editor', () => {
|
|
608
|
+
const out = mapRcsCardContentForConsumerWithResolvedTags([
|
|
609
|
+
{
|
|
610
|
+
title: '',
|
|
611
|
+
description: 'Visit {{gt}} discount',
|
|
612
|
+
mediaType: 'IMAGE',
|
|
613
|
+
cardVarMapped: { gt: '{{loyalty_points}}', 1: '{{loyalty_points}}' },
|
|
614
|
+
},
|
|
615
|
+
]);
|
|
616
|
+
expect(out[0].title).toBe('');
|
|
617
|
+
// No resolution happens here — the tag reference lives only in cardVarMapped, never baked
|
|
618
|
+
// into the text (that used to replace {{gt}} with {{loyalty_points}}).
|
|
619
|
+
expect(out[0].description).toBe('Visit {{gt}} discount');
|
|
620
|
+
expect(out[0].cardVarMapped).toEqual({ gt: '{{loyalty_points}}', 1: '{{loyalty_points}}' });
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('forces title to empty for text-only cards (mediaType NONE/TEXT), matching the editor\'s own display', () => {
|
|
624
|
+
const out = mapRcsCardContentForConsumerWithResolvedTags([
|
|
625
|
+
{
|
|
626
|
+
title: 'Stale title',
|
|
627
|
+
description: 'Hi {{1}}',
|
|
628
|
+
mediaType: 'NONE',
|
|
629
|
+
cardVarMapped: { 1: 'test' },
|
|
630
|
+
},
|
|
631
|
+
]);
|
|
620
632
|
expect(out[0].title).toBe('');
|
|
621
|
-
expect(out[0].description).toBe('
|
|
633
|
+
expect(out[0].description).toBe('Hi {{1}}');
|
|
622
634
|
});
|
|
623
635
|
|
|
624
|
-
it('
|
|
636
|
+
it('ignores any extra legacy args (root map / isFullMode) — only this card\'s own cardVarMapped is used', () => {
|
|
625
637
|
const out = mapRcsCardContentForConsumerWithResolvedTags(
|
|
626
638
|
[
|
|
627
639
|
{
|
|
628
|
-
title: 'Hi {{
|
|
629
|
-
description: 'Pts {{
|
|
640
|
+
title: 'Hi {{1}}',
|
|
641
|
+
description: 'Pts {{2}}',
|
|
630
642
|
mediaType: 'IMAGE',
|
|
631
|
-
cardVarMapped: {
|
|
632
|
-
promotion_points: '{{loyalty_points}}',
|
|
633
|
-
2: '{{loyalty_points}}',
|
|
634
|
-
},
|
|
643
|
+
cardVarMapped: { 1: '{{first_name}}', 2: '{{loyalty_points}}' },
|
|
635
644
|
},
|
|
636
645
|
],
|
|
637
|
-
{
|
|
638
|
-
first_name: '{{customer_name}}',
|
|
639
|
-
1: '{{customer_name}}',
|
|
640
|
-
},
|
|
646
|
+
{ 1: '{{customer_name}}' },
|
|
641
647
|
false,
|
|
642
648
|
);
|
|
643
|
-
expect(out[0].title).toBe('Hi {{
|
|
644
|
-
expect(out[0].description).toBe('Pts {{
|
|
649
|
+
expect(out[0].title).toBe('Hi {{1}}');
|
|
650
|
+
expect(out[0].description).toBe('Pts {{2}}');
|
|
651
|
+
expect(out[0].cardVarMapped).toEqual({ 1: '{{first_name}}', 2: '{{loyalty_points}}' });
|
|
645
652
|
});
|
|
646
653
|
|
|
647
654
|
it('passes through null or non-object items in the card array unchanged', () => {
|
|
648
|
-
const out = mapRcsCardContentForConsumerWithResolvedTags([null, 'string', 42]
|
|
655
|
+
const out = mapRcsCardContentForConsumerWithResolvedTags([null, 'string', 42]);
|
|
649
656
|
expect(out).toEqual([null, 'string', 42]);
|
|
650
657
|
});
|
|
651
658
|
|
|
652
|
-
it('preserves a voucher/offer tag value (parenthesized key) in the emitted cardVarMapped, and keeps the raw
|
|
653
|
-
const out = mapRcsCardContentForConsumerWithResolvedTags(
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
//
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
expect(out[0].description).toBe('This is Bird with colorful feathers {{voucher(1190738)}}');
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
it('BUG REPRO: 2-card carousel — card 1 description resolves to card 0\'s value instead of its own tag pick', () => {
|
|
673
|
-
// Mirrors CreativesContainer/index.js ~1448-1461: rootRcsCardVarMapped is derived ONLY from
|
|
674
|
-
// cardContentFromSubmit[0] (the FIRST card's own cardVarMapped) and reused for every card.
|
|
675
|
-
// Card 0: title slot 1 -> user_id_b64, desc slot 2 -> voucher(1190738).
|
|
676
|
-
// Card 1: title slot 3 -> user_id_b64 (same semantic name as card 0), desc slot 4 -> first_name
|
|
677
|
-
// (the user's actual pick for card 1's own description slot, via "Add labels" while focused on card 1).
|
|
659
|
+
it('preserves a voucher/offer tag value (parenthesized key) in the emitted cardVarMapped, and keeps the raw {{N}} slot in the description', () => {
|
|
660
|
+
const out = mapRcsCardContentForConsumerWithResolvedTags([
|
|
661
|
+
{
|
|
662
|
+
title: '',
|
|
663
|
+
description: 'This is Bird with colorful feathers {{1}}',
|
|
664
|
+
mediaType: 'NONE',
|
|
665
|
+
cardVarMapped: { 'voucher(1190738)': 'SAVE20', 1: 'SAVE20' },
|
|
666
|
+
},
|
|
667
|
+
]);
|
|
668
|
+
expect(out[0].cardVarMapped).toEqual({ 'voucher(1190738)': 'SAVE20', 1: 'SAVE20' });
|
|
669
|
+
expect(out[0].description).toBe('This is Bird with colorful feathers {{1}}');
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it('carousel: each card keeps its own numeric slots and its own cardVarMapped — no cross-card leak', () => {
|
|
673
|
+
// Previously, CreativesContainer derived a "root" map from card 0 only and merged it into
|
|
674
|
+
// every card's resolution, which could leak card 0's values into card 1's text. Since text is
|
|
675
|
+
// no longer resolved at all, that leak class is gone: each card's output is self-contained.
|
|
678
676
|
const card0 = {
|
|
679
677
|
title: 'This is a bird {{1}}',
|
|
680
678
|
description: 'This is Bird with colorful feathers {{2}}',
|
|
@@ -687,29 +685,16 @@ describe('RCS utils', () => {
|
|
|
687
685
|
mediaType: 'IMAGE',
|
|
688
686
|
cardVarMapped: { 3: '{{user_id_b64}}', 4: '{{first_name}}' },
|
|
689
687
|
};
|
|
690
|
-
// rootRcsCardVarMapped, as CreativesContainer actually builds it: card 0's own map only.
|
|
691
|
-
const rootRcsCardVarMappedFromFirstCardOnly = { 1: '{{user_id_b64}}', 2: '{{voucher(1190738)}}' };
|
|
692
|
-
|
|
693
|
-
const out = mapRcsCardContentForConsumerWithResolvedTags(
|
|
694
|
-
[card0, card1],
|
|
695
|
-
rootRcsCardVarMappedFromFirstCardOnly,
|
|
696
|
-
true,
|
|
697
|
-
);
|
|
698
688
|
|
|
699
|
-
|
|
700
|
-
expect(out[0].title).toBe('This is a bird {{user_id_b64}}');
|
|
701
|
-
expect(out[0].description).toBe('This is Bird with colorful feathers {{voucher(1190738)}}');
|
|
689
|
+
const out = mapRcsCardContentForConsumerWithResolvedTags([card0, card1]);
|
|
702
690
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
expect(out[
|
|
691
|
+
expect(out[0].title).toBe('This is a bird {{1}}');
|
|
692
|
+
expect(out[0].description).toBe('This is Bird with colorful feathers {{2}}');
|
|
693
|
+
expect(out[0].cardVarMapped).toEqual({ 1: '{{user_id_b64}}', 2: '{{voucher(1190738)}}' });
|
|
706
694
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
// 0 for every card (utils.js ~345-351) while the merged map still carries card 0's leftover
|
|
711
|
-
// global-numbered keys (via the root map bug in CreativesContainer).
|
|
712
|
-
expect(out[1].description).toBe('This park has a cat and a bird {{first_name}}');
|
|
695
|
+
expect(out[1].title).toBe('This is a park {{3}}');
|
|
696
|
+
expect(out[1].description).toBe('This park has a cat and a bird {{4}}');
|
|
697
|
+
expect(out[1].cardVarMapped).toEqual({ 3: '{{user_id_b64}}', 4: '{{first_name}}' });
|
|
713
698
|
});
|
|
714
699
|
});
|
|
715
700
|
|
|
@@ -360,53 +360,28 @@ export function resolveRcsCardPreviewStrings(
|
|
|
360
360
|
}
|
|
361
361
|
|
|
362
362
|
/**
|
|
363
|
-
* Campaign consumer payload:
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
363
|
+
* Campaign consumer payload: pass each card's `title` / `description` through unchanged — per the
|
|
364
|
+
* API contract (see `createPayload`), they only ever contain sequential numeric slot placeholders
|
|
365
|
+
* (`{{1}}`, `{{2}}`, …), never a tag name or literal value baked into the text. Personalization tag
|
|
366
|
+
* references and literal values alike live solely in the emitted `cardVarMapped`, so the backend
|
|
367
|
+
* resolves every slot from that map. `cardVarMapped` is filtered to this card's own numeric/semantic
|
|
368
|
+
* entries via `pickRcsCardVarMappedEntries` (drops stray/SMS-fallback keys). Text-only cards
|
|
369
|
+
* (`mediaType` NONE/TEXT) never surface a title to consumers, matching the editor's own display.
|
|
367
370
|
*/
|
|
368
|
-
export function mapRcsCardContentForConsumerWithResolvedTags(
|
|
369
|
-
cardContentArray,
|
|
370
|
-
rootRcsCardVarMapped,
|
|
371
|
-
isFullMode,
|
|
372
|
-
) {
|
|
373
|
-
const rootRecord =
|
|
374
|
-
rootRcsCardVarMapped != null && typeof rootRcsCardVarMapped === 'object'
|
|
375
|
-
? rootRcsCardVarMapped
|
|
376
|
-
: {};
|
|
371
|
+
export function mapRcsCardContentForConsumerWithResolvedTags(cardContentArray) {
|
|
377
372
|
const list = Array.isArray(cardContentArray) ? cardContentArray : [];
|
|
378
|
-
const isLibraryMode = isFullMode !== true;
|
|
379
|
-
let carouselSlotOffset = 0;
|
|
380
373
|
return list.map((card) => {
|
|
381
374
|
if (!card || typeof card !== 'object') return card;
|
|
382
375
|
const nested =
|
|
383
376
|
card.cardVarMapped != null && typeof card.cardVarMapped === 'object'
|
|
384
377
|
? card.cardVarMapped
|
|
385
378
|
: {};
|
|
386
|
-
const rootClean = pickRcsCardVarMappedEntries(rootRecord);
|
|
387
379
|
const nestedClean = pickRcsCardVarMappedEntries(nested);
|
|
388
|
-
const merged = { ...rootClean, ...nestedClean };
|
|
389
380
|
const textOnly = isRcsTextOnlyCardMediaType(card.mediaType);
|
|
390
|
-
const cardTitle = card.title ?? '';
|
|
391
|
-
const cardDescription = card.description ?? '';
|
|
392
|
-
const { rcsTitle, rcsDesc } = resolveRcsCardPreviewStrings(
|
|
393
|
-
cardTitle,
|
|
394
|
-
cardDescription,
|
|
395
|
-
merged,
|
|
396
|
-
isLibraryMode,
|
|
397
|
-
textOnly,
|
|
398
|
-
true,
|
|
399
|
-
carouselSlotOffset,
|
|
400
|
-
);
|
|
401
|
-
const effectiveTitleForCount = textOnly ? '' : String(cardTitle);
|
|
402
|
-
const titleVarCount = (effectiveTitleForCount.match(rcsVarRegex) || []).length;
|
|
403
|
-
const descVarCount = (String(cardDescription).match(rcsVarRegex) || []).length;
|
|
404
|
-
carouselSlotOffset += titleVarCount + descVarCount;
|
|
405
381
|
const { cardVarMapped: _drop, ...cardRest } = card;
|
|
406
382
|
return {
|
|
407
383
|
...cardRest,
|
|
408
|
-
title:
|
|
409
|
-
description: rcsDesc,
|
|
384
|
+
...(textOnly ? { title: '' } : {}),
|
|
410
385
|
...(Object.keys(nestedClean).length > 0 ? { cardVarMapped: nestedClean } : {}),
|
|
411
386
|
};
|
|
412
387
|
});
|