@capillarytech/creatives-library 9.0.13 → 9.0.14

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 (38) hide show
  1. package/package.json +1 -1
  2. package/services/api.js +10 -0
  3. package/services/tests/api.test.js +83 -0
  4. package/v2Components/CommonTestAndPreview/UnifiedPreview/WhatsAppPreviewContent.js +5 -3
  5. package/v2Components/CommonTestAndPreview/index.js +7 -0
  6. package/v2Components/NavigationBar/index.js +27 -0
  7. package/v2Components/NavigationBar/messages.js +4 -0
  8. package/v2Components/NavigationBar/tests/index.test.js +19 -0
  9. package/v2Components/NewCallTask/index.js +6 -1
  10. package/v2Components/TemplatePreview/index.js +4 -2
  11. package/v2Containers/Cap/index.js +3 -1
  12. package/v2Containers/CommunicationFlow/CommunicationFlow.js +130 -20
  13. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +154 -0
  14. package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +240 -0
  15. package/v2Containers/CommunicationFlow/DemoPage.js +47 -0
  16. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +369 -2
  17. package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +619 -0
  18. package/v2Containers/CommunicationFlow/Tests/DemoPage.test.js +77 -0
  19. package/v2Containers/CommunicationFlow/Tests/getContentBody.test.js +933 -0
  20. package/v2Containers/CommunicationFlow/constants.js +45 -10
  21. package/v2Containers/CommunicationFlow/index.js +5 -2
  22. package/v2Containers/CommunicationFlow/messages.js +20 -0
  23. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +94 -31
  24. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +14 -11
  25. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +1144 -32
  26. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/extractContentForPreview.js +183 -0
  27. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/CommunicationStrategyStep.js +3 -0
  28. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +39 -0
  29. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +6 -2
  30. package/v2Containers/CommunicationFlow/utils/getContentBody.js +369 -0
  31. package/v2Containers/CommunicationFlow/utils/getContentBody.scss +19 -0
  32. package/v2Containers/CommunicationFlow/utils/getEnabledSteps.js +1 -1
  33. package/v2Containers/CreativesContainer/constants.js +6 -0
  34. package/v2Containers/CreativesContainer/index.js +68 -1
  35. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +2 -2
  36. package/v2Containers/Templates/index.js +2 -2
  37. package/v2Containers/TemplatesV2/index.js +9 -1
  38. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +41 -34
@@ -1,5 +1,10 @@
1
1
  import React from 'react';
2
2
 
3
+ jest.mock('../../../services/api', () => ({
4
+ createCentralCommsMetaId: jest.fn(),
5
+ getCentralCommsMetaIds: jest.fn(),
6
+ }));
7
+
3
8
  jest.mock('../../CreativesContainer', () => function MockCreativesContainer({
4
9
  getCreativesData,
5
10
  handleCloseCreatives,
@@ -33,6 +38,7 @@ import { IntlProvider } from 'react-intl';
33
38
  import history from '../../../utils/history';
34
39
  import { initialReducer } from '../../../initialReducer';
35
40
  import CommunicationFlow from '../CommunicationFlow';
41
+ import { createCentralCommsMetaId, getCentralCommsMetaIds } from '../../../services/api';
36
42
  import { getEnabledSteps } from '../utils/getEnabledSteps';
37
43
  import {
38
44
  CHANNELS,
@@ -87,7 +93,8 @@ const FEATURES = {
87
93
  strategyContentDynamic: {
88
94
  communicationStrategyData: { required: true, options: STRATEGY_OPTIONS, disabled: false },
89
95
  contentTemplateData: { required: true, channels: CHANNELS },
90
- dynamicControlsData: { required: true, controls: DYNAMIC_CONTROLS },
96
+ showDynamicControls: true,
97
+ dynamicControlsData: { controls: DYNAMIC_CONTROLS },
91
98
  },
92
99
  };
93
100
 
@@ -212,7 +219,7 @@ describe('CommunicationFlow', () => {
212
219
 
213
220
  await selectCommunicationStrategy('Single template');
214
221
 
215
- await userEvent.click(screen.getByRole('button', { name: /content template/i }));
222
+ await userEvent.click(screen.getByRole('button', { name: /add creative/i }));
216
223
  await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
217
224
  await userEvent.click(within(screen.getByRole('menu')).getByText('SMS'));
218
225
 
@@ -257,4 +264,364 @@ describe('CommunicationFlow', () => {
257
264
  });
258
265
  expect(screen.getByRole('radio', { name: /transactional/i })).toBeChecked();
259
266
  });
267
+
268
+ it('initializes messageType from messageTypeData.defaultOption.value when initialData absent', () => {
269
+ renderWithFlow({
270
+ features: {
271
+ messageTypeData: {
272
+ required: true,
273
+ options: MESSAGE_OPTIONS,
274
+ defaultOption: { value: 'transactional', label: 'Transactional' },
275
+ },
276
+ },
277
+ });
278
+ expect(screen.getByRole('radio', { name: /transactional/i })).toBeChecked();
279
+ });
280
+ });
281
+
282
+ describe('isSaveDisabled', () => {
283
+ it('is false with empty features — Save button is enabled', () => {
284
+ renderWithFlow({ features: {} });
285
+ expect(screen.getByRole('button', { name: /^save$/i })).not.toBeDisabled();
286
+ });
287
+
288
+ it('is true when communicationStrategy required and not yet selected', () => {
289
+ renderWithFlow({ features: FEATURES.strategyContent });
290
+ expect(screen.getByRole('button', { name: /^save$/i })).toBeDisabled();
291
+ });
292
+
293
+ it('is true when channel selection required, single-channel, and contentItems empty', () => {
294
+ renderWithFlow({
295
+ features: FEATURES.strategyContent,
296
+ initialData: { communicationStrategy: SINGLE_TEMPLATE, contentItems: [] },
297
+ });
298
+ expect(screen.getByRole('button', { name: /^save$/i })).toBeDisabled();
299
+ });
300
+
301
+ it('is false for multi-channel strategy even without content items', () => {
302
+ renderWithFlow({
303
+ features: FEATURES.strategyContent,
304
+ initialData: { communicationStrategy: CHANNEL_PRIORITY, contentItems: [] },
305
+ });
306
+ expect(screen.getByRole('button', { name: /^save$/i })).not.toBeDisabled();
307
+ });
308
+
309
+ it('is true when delivery settings enabled and a channel is missing sender details', () => {
310
+ renderWithFlow({
311
+ features: {
312
+ communicationStrategyData: { required: true, options: STRATEGY_OPTIONS },
313
+ contentTemplateData: { required: true, channels: CHANNELS },
314
+ deliverySettingsData: {},
315
+ },
316
+ initialData: {
317
+ communicationStrategy: SINGLE_TEMPLATE,
318
+ contentItems: [{ channel: 'SMS', templateData: {} }],
319
+ deliverySetting: {},
320
+ },
321
+ });
322
+ expect(screen.getByRole('button', { name: /^save$/i })).toBeDisabled();
323
+ });
324
+
325
+ it('is false when delivery settings enabled and all channels have sender details', () => {
326
+ renderWithFlow({
327
+ features: {
328
+ communicationStrategyData: { required: true, options: STRATEGY_OPTIONS },
329
+ contentTemplateData: { required: true, channels: CHANNELS },
330
+ deliverySettingsData: {},
331
+ },
332
+ initialData: {
333
+ communicationStrategy: SINGLE_TEMPLATE,
334
+ contentItems: [{ channel: 'SMS', templateData: {} }],
335
+ deliverySetting: { channelSetting: { SMS: { gsmSenderId: 'CAPS_SENDER' } } },
336
+ },
337
+ });
338
+ expect(screen.getByRole('button', { name: /^save$/i })).not.toBeDisabled();
339
+ });
340
+
341
+ it('skips delivery check for CHANNELS_WITHOUT_DELIVERY (e.g. MOBILEPUSH)', () => {
342
+ renderWithFlow({
343
+ features: {
344
+ communicationStrategyData: { required: true, options: STRATEGY_OPTIONS },
345
+ contentTemplateData: { required: true, channels: CHANNELS },
346
+ deliverySettingsData: {},
347
+ },
348
+ initialData: {
349
+ communicationStrategy: SINGLE_TEMPLATE,
350
+ contentItems: [{ channel: 'MOBILEPUSH', templateData: {} }],
351
+ deliverySetting: {},
352
+ },
353
+ });
354
+ expect(screen.getByRole('button', { name: /^save$/i })).not.toBeDisabled();
355
+ });
356
+ });
357
+
358
+ describe('handleSave — CCS flow', () => {
359
+ beforeEach(() => {
360
+ createCentralCommsMetaId.mockResolvedValue({ response: { data: { id: 'meta-123' } } });
361
+ getCentralCommsMetaIds.mockResolvedValue({ response: { data: {} } });
362
+ });
363
+
364
+ afterEach(() => {
365
+ jest.clearAllMocks();
366
+ });
367
+
368
+ it('calls createCentralCommsMetaId for each content item when useCCS is not false', async () => {
369
+ const onSave = jest.fn();
370
+ renderWithFlow({
371
+ features: {},
372
+ initialData: {
373
+ contentItems: [{ channel: 'sms', templateData: { smsBody: 'Hello' } }],
374
+ },
375
+ onSave,
376
+ });
377
+
378
+ await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
379
+
380
+ await waitFor(() => expect(createCentralCommsMetaId).toHaveBeenCalledTimes(1));
381
+ expect(createCentralCommsMetaId).toHaveBeenCalledWith(
382
+ expect.objectContaining({
383
+ centralCommsPayload: expect.objectContaining({ channel: 'SMS', module: 'CAMPAIGNS' }),
384
+ }),
385
+ );
386
+ expect(onSave).toHaveBeenCalledTimes(1);
387
+ });
388
+
389
+ it('calls getCentralCommsMetaIds when metaIds are returned from createCentralCommsMetaId', async () => {
390
+ renderWithFlow({
391
+ features: {},
392
+ initialData: { contentItems: [{ channel: 'EMAIL', templateData: {} }] },
393
+ });
394
+
395
+ await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
396
+
397
+ await waitFor(() => expect(getCentralCommsMetaIds).toHaveBeenCalledWith('meta-123'));
398
+ });
399
+
400
+ it('skips getCentralCommsMetaIds when response contains no id', async () => {
401
+ createCentralCommsMetaId.mockResolvedValue({ response: { data: {} } });
402
+ renderWithFlow({
403
+ features: {},
404
+ initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
405
+ });
406
+
407
+ await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
408
+
409
+ await waitFor(() => expect(createCentralCommsMetaId).toHaveBeenCalled());
410
+ expect(getCentralCommsMetaIds).not.toHaveBeenCalled();
411
+ });
412
+
413
+ it('uses ouId and module from config.context when provided', async () => {
414
+ renderWithFlow({
415
+ features: {},
416
+ config: { ...baseConfig, context: { ouId: 42, module: 'LOYALTY' }, features: {} },
417
+ initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
418
+ });
419
+
420
+ await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
421
+
422
+ await waitFor(() => expect(createCentralCommsMetaId).toHaveBeenCalled());
423
+ expect(createCentralCommsMetaId).toHaveBeenCalledWith(
424
+ expect.objectContaining({
425
+ centralCommsPayload: expect.objectContaining({ ouId: 42, module: 'LOYALTY' }),
426
+ }),
427
+ );
428
+ });
429
+
430
+ it('skips CCS entirely when useCCS is false', async () => {
431
+ const onSave = jest.fn();
432
+ renderWithFlow({
433
+ features: {},
434
+ config: { ...baseConfig, useCCS: false, features: {} },
435
+ initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
436
+ onSave,
437
+ });
438
+
439
+ await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
440
+
441
+ await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
442
+ expect(createCentralCommsMetaId).not.toHaveBeenCalled();
443
+ });
444
+
445
+ it('still calls onSave when createCentralCommsMetaId rejects', async () => {
446
+ createCentralCommsMetaId.mockRejectedValue(new Error('Network error'));
447
+ const onSave = jest.fn();
448
+ renderWithFlow({
449
+ features: {},
450
+ initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
451
+ onSave,
452
+ });
453
+
454
+ await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
455
+
456
+ await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
457
+ });
458
+
459
+ it('skips createCentralCommsMetaId when contentItems is empty', async () => {
460
+ const onSave = jest.fn();
461
+ renderWithFlow({
462
+ features: {},
463
+ initialData: { contentItems: [] },
464
+ onSave,
465
+ });
466
+
467
+ await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
468
+
469
+ await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
470
+ expect(createCentralCommsMetaId).not.toHaveBeenCalled();
471
+ });
472
+
473
+ it('includes additionalSettings derived from dynamicControls in the payload', async () => {
474
+ renderWithFlow({
475
+ features: {},
476
+ initialData: {
477
+ contentItems: [{ channel: 'SMS', templateData: {} }],
478
+ dynamicControls: {
479
+ useTinyUrl: true,
480
+ sendToControlCustomers: true,
481
+ overrideDailyLimit: true,
482
+ sendToBrandPocs: true,
483
+ },
484
+ },
485
+ });
486
+
487
+ await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
488
+
489
+ await waitFor(() => expect(createCentralCommsMetaId).toHaveBeenCalled());
490
+ const payload = createCentralCommsMetaId.mock.calls[0][0];
491
+ expect(payload.centralCommsPayload.smsDeliverySettings.additionalSettings).toEqual({
492
+ useTinyUrl: true,
493
+ encryptUrl: true,
494
+ linkTrackingEnabled: true,
495
+ userSubscriptionDisabled: true,
496
+ });
497
+ });
498
+ });
499
+
500
+ describe('renderSteps — null returns and edge cases', () => {
501
+ it('returns null for CHANNEL_SELECTION when communicationStrategy is null', () => {
502
+ renderWithFlow({
503
+ features: {
504
+ communicationStrategyData: { required: true, options: STRATEGY_OPTIONS },
505
+ contentTemplateData: { required: true, channels: CHANNELS },
506
+ },
507
+ });
508
+ expect(screen.queryByText(/content template/i)).not.toBeInTheDocument();
509
+ });
510
+
511
+ it('returns null for DYNAMIC_CONTROLS when communicationStrategy is null', () => {
512
+ renderWithFlow({ features: FEATURES.strategyContentDynamic });
513
+ expect(screen.queryByText(/other controls/i)).not.toBeInTheDocument();
514
+ });
515
+
516
+ it('does not render the Save footer when onSave is not provided', () => {
517
+ renderFlow(
518
+ <CommunicationFlow
519
+ config={baseConfig}
520
+ onCancel={jest.fn()}
521
+ capData={{}}
522
+ />,
523
+ );
524
+ expect(screen.queryByRole('button', { name: /^save$/i })).not.toBeInTheDocument();
525
+ });
526
+
527
+ it('passes cap prop to ChannelSelectionStep over capData when both provided', async () => {
528
+ const capOverride = { orgId: 999 };
529
+ renderWithFlow({
530
+ features: FEATURES.strategyContent,
531
+ initialData: { communicationStrategy: SINGLE_TEMPLATE },
532
+ cap: capOverride,
533
+ });
534
+ // Renders without crash and channel step is visible after strategy selection
535
+ expect(screen.queryByText(/communication strategy/i)).toBeInTheDocument();
536
+ });
537
+
538
+ it('renders DYNAMIC_CONTROLS step after communicationStrategy is selected', async () => {
539
+ renderWithFlow({ features: FEATURES.strategyContentDynamic });
540
+ await selectCommunicationStrategy('Single template');
541
+ await waitFor(() => {
542
+ expect(screen.getByText(/other controls/i)).toBeInTheDocument();
543
+ });
544
+ });
545
+ });
546
+
547
+ describe('optional chaining safety', () => {
548
+ afterEach(() => {
549
+ jest.clearAllMocks();
550
+ });
551
+
552
+ it('defaults ouId to -1 when config.context is absent', async () => {
553
+ createCentralCommsMetaId.mockResolvedValue({ response: { data: { id: 'x' } } });
554
+ renderWithFlow({
555
+ features: {},
556
+ initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
557
+ });
558
+
559
+ await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
560
+
561
+ await waitFor(() => expect(createCentralCommsMetaId).toHaveBeenCalled());
562
+ expect(createCentralCommsMetaId).toHaveBeenCalledWith(
563
+ expect.objectContaining({
564
+ centralCommsPayload: expect.objectContaining({ ouId: -1 }),
565
+ }),
566
+ );
567
+ });
568
+
569
+ it('defaults module to consumer.toUpperCase() when config.context.module absent', async () => {
570
+ createCentralCommsMetaId.mockResolvedValue({ response: { data: { id: 'x' } } });
571
+ renderWithFlow({
572
+ features: {},
573
+ config: { ...baseConfig, consumer: 'loyalty', features: {} },
574
+ initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
575
+ });
576
+
577
+ await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
578
+
579
+ await waitFor(() => expect(createCentralCommsMetaId).toHaveBeenCalled());
580
+ expect(createCentralCommsMetaId).toHaveBeenCalledWith(
581
+ expect.objectContaining({
582
+ centralCommsPayload: expect.objectContaining({ module: 'LOYALTY' }),
583
+ }),
584
+ );
585
+ });
586
+
587
+ it('initializes channel from config.channel when initialData.channel is absent', () => {
588
+ renderWithFlow({
589
+ features: {},
590
+ config: { ...baseConfig, channel: 'EMAIL', features: {} },
591
+ initialData: {},
592
+ });
593
+ expect(screen.getByRole('button', { name: /^save$/i })).toBeInTheDocument();
594
+ });
595
+
596
+ it('renders without crash when config.features is absent', () => {
597
+ renderWithFlow({ config: { ...baseConfig, features: undefined } });
598
+ expect(screen.getByRole('button', { name: /^save$/i })).toBeInTheDocument();
599
+ });
600
+
601
+ it('handles onChange being null without crashing on step data change', async () => {
602
+ renderWithFlow({
603
+ features: FEATURES.messageType,
604
+ onChange: null,
605
+ });
606
+ await expect(
607
+ userEvent.click(screen.getByRole('radio', { name: /transactional/i })),
608
+ ).resolves.not.toThrow();
609
+ });
610
+
611
+ it('handles deliverySetting?.channelSetting absent without crashing in isSaveDisabled', () => {
612
+ renderWithFlow({
613
+ features: {
614
+ communicationStrategyData: { required: true, options: STRATEGY_OPTIONS },
615
+ contentTemplateData: { required: true, channels: CHANNELS },
616
+ deliverySettingsData: {},
617
+ },
618
+ initialData: {
619
+ communicationStrategy: SINGLE_TEMPLATE,
620
+ contentItems: [{ channel: 'SMS', templateData: {} }],
621
+ deliverySetting: null,
622
+ },
623
+ });
624
+ // Missing deliverySetting → channelSetting defaults to {} → SMS missing → disabled
625
+ expect(screen.getByRole('button', { name: /^save$/i })).toBeDisabled();
626
+ });
260
627
  });