@capillarytech/creatives-library 9.0.14-beta.0 → 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 (83) hide show
  1. package/constants/unified.js +0 -3
  2. package/package.json +1 -1
  3. package/services/api.js +10 -0
  4. package/services/tests/api.test.js +83 -0
  5. package/utils/common.js +0 -8
  6. package/v2Components/CommonTestAndPreview/UnifiedPreview/WhatsAppPreviewContent.js +5 -3
  7. package/v2Components/CommonTestAndPreview/index.js +7 -0
  8. package/v2Components/FormBuilder/_formBuilder.scss +0 -5
  9. package/v2Components/FormBuilder/index.js +4479 -41
  10. package/v2Components/NavigationBar/index.js +27 -0
  11. package/v2Components/NavigationBar/messages.js +4 -0
  12. package/v2Components/NavigationBar/tests/index.test.js +19 -0
  13. package/v2Components/NewCallTask/index.js +6 -1
  14. package/v2Components/TemplatePreview/index.js +4 -2
  15. package/v2Containers/Cap/index.js +3 -1
  16. package/v2Containers/CommunicationFlow/CommunicationFlow.js +130 -20
  17. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +154 -0
  18. package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +240 -0
  19. package/v2Containers/CommunicationFlow/DemoPage.js +47 -0
  20. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +369 -2
  21. package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +619 -0
  22. package/v2Containers/CommunicationFlow/Tests/DemoPage.test.js +77 -0
  23. package/v2Containers/CommunicationFlow/Tests/getContentBody.test.js +933 -0
  24. package/v2Containers/CommunicationFlow/constants.js +45 -10
  25. package/v2Containers/CommunicationFlow/index.js +5 -2
  26. package/v2Containers/CommunicationFlow/messages.js +20 -0
  27. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +94 -31
  28. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +14 -11
  29. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +1144 -32
  30. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/extractContentForPreview.js +183 -0
  31. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/CommunicationStrategyStep.js +3 -0
  32. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +39 -0
  33. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +6 -2
  34. package/v2Containers/CommunicationFlow/utils/getContentBody.js +369 -0
  35. package/v2Containers/CommunicationFlow/utils/getContentBody.scss +19 -0
  36. package/v2Containers/CommunicationFlow/utils/getEnabledSteps.js +1 -1
  37. package/v2Containers/CreativesContainer/constants.js +6 -0
  38. package/v2Containers/CreativesContainer/index.js +68 -1
  39. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +2 -2
  40. package/v2Containers/Templates/index.js +2 -2
  41. package/v2Containers/TemplatesV2/index.js +9 -1
  42. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +41 -34
  43. package/v2Components/FormBuilder/Classic.js +0 -4487
  44. package/v2Components/FormBuilder/Functional/FormBuilderShell.js +0 -371
  45. package/v2Components/FormBuilder/Functional/channels/registry.js +0 -17
  46. package/v2Components/FormBuilder/Functional/channels/sms/buildSubmitPayload.js +0 -9
  47. package/v2Components/FormBuilder/Functional/channels/sms/config.js +0 -30
  48. package/v2Components/FormBuilder/Functional/channels/sms/getEditorErrorDescriptor.js +0 -46
  49. package/v2Components/FormBuilder/Functional/channels/sms/getLiquidContent.js +0 -13
  50. package/v2Components/FormBuilder/Functional/channels/sms/index.js +0 -22
  51. package/v2Components/FormBuilder/Functional/channels/sms/tests/getEditorErrorDescriptor.test.js +0 -52
  52. package/v2Components/FormBuilder/Functional/channels/sms/tests/getLiquidContent.test.js +0 -25
  53. package/v2Components/FormBuilder/Functional/channels/sms/tests/validate.test.js +0 -87
  54. package/v2Components/FormBuilder/Functional/channels/sms/validate.js +0 -89
  55. package/v2Components/FormBuilder/Functional/constants.js +0 -42
  56. package/v2Components/FormBuilder/Functional/core/schema/fieldRegistry.js +0 -38
  57. package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +0 -85
  58. package/v2Components/FormBuilder/Functional/core/store/formReducer.js +0 -81
  59. package/v2Components/FormBuilder/Functional/core/store/selectors.js +0 -30
  60. package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +0 -91
  61. package/v2Components/FormBuilder/Functional/index.js +0 -26
  62. package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +0 -59
  63. package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +0 -31
  64. package/v2Components/FormBuilder/Functional/layout/Section.js +0 -116
  65. package/v2Components/FormBuilder/Functional/renderers/smsRenderers.js +0 -258
  66. package/v2Components/FormBuilder/Functional/tests/channelRegistry.test.js +0 -21
  67. package/v2Components/FormBuilder/Functional/tests/fieldRegistry.test.js +0 -65
  68. package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +0 -97
  69. package/v2Components/FormBuilder/Functional/tests/fixtures/smsParityCases.js +0 -192
  70. package/v2Components/FormBuilder/Functional/tests/formReducer.test.js +0 -129
  71. package/v2Components/FormBuilder/Functional/tests/initializeFormState.test.js +0 -132
  72. package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +0 -40
  73. package/v2Components/FormBuilder/Functional/tests/section.test.js +0 -99
  74. package/v2Components/FormBuilder/Functional/tests/selectors.test.js +0 -67
  75. package/v2Components/FormBuilder/Functional/tests/sms.crossFlowParity.test.js +0 -155
  76. package/v2Components/FormBuilder/Functional/tests/sms.liquid.test.js +0 -172
  77. package/v2Components/FormBuilder/Functional/tests/sms.rollout.test.js +0 -122
  78. package/v2Components/FormBuilder/Functional/tests/sms.shell.parity.test.js +0 -329
  79. package/v2Components/FormBuilder/Functional/tests/smsRenderers.test.js +0 -160
  80. package/v2Components/FormBuilder/Functional/tests/toLegacyFormData.test.js +0 -95
  81. package/v2Components/FormBuilder/tests/__snapshots__/sms.characterization.test.js.snap +0 -114
  82. package/v2Components/FormBuilder/tests/entryGate.test.js +0 -106
  83. package/v2Components/FormBuilder/tests/sms.characterization.test.js +0 -336
@@ -17,6 +17,21 @@ jest.setTimeout(30000);
17
17
  // Larger wait budget for individual menu/portal transitions under parallel load.
18
18
  const WAIT_OPTIONS = { timeout: 10000 };
19
19
 
20
+ jest.mock('v2Components/TestAndPreviewSlidebox', () => function MockTestAndPreviewSlidebox({ show, onClose, channel }) {
21
+ if (!show) return null;
22
+ return (
23
+ <div data-testid="test-and-preview-mock" data-channel={channel}>
24
+ <button type="button" data-testid="tap-close" onClick={onClose}>Close preview</button>
25
+ </div>
26
+ );
27
+ });
28
+
29
+ jest.mock('../../../../Whatsapp/utils', () => ({
30
+ getWhatsappDocPreview: jest.fn(() => ({ docType: 'pdf', docUrl: 'http://doc' })),
31
+ getWhatsappQuickReply: jest.fn(() => null),
32
+ getWhatsappCarouselButtonView: jest.fn(() => null),
33
+ }));
34
+
20
35
  jest.mock('../../../../CreativesContainer', () => function MockCreativesContainer({
21
36
  getCreativesData,
22
37
  handleCloseCreatives,
@@ -90,8 +105,7 @@ describe('ChannelSelectionStep', () => {
90
105
  );
91
106
 
92
107
  expect(screen.getAllByText('Content template').length).toBeGreaterThanOrEqual(1);
93
- expect(screen.getByText('Add message content and incentive')).toBeInTheDocument();
94
- expect(screen.getByRole('button', { name: /content template/i })).toBeInTheDocument();
108
+ expect(screen.getByRole('button', { name: /add creative/i })).toBeInTheDocument();
95
109
  expect(screen.queryByTestId('creatives-mock')).not.toBeInTheDocument();
96
110
  });
97
111
 
@@ -150,7 +164,7 @@ describe('ChannelSelectionStep', () => {
150
164
  />,
151
165
  );
152
166
 
153
- await userEvent.click(screen.getByRole('button', { name: /content template/i }));
167
+ await userEvent.click(screen.getByRole('button', { name: /add creative/i }));
154
168
  await waitFor(() => {
155
169
  expect(screen.getByRole('menu')).toBeInTheDocument();
156
170
  }, WAIT_OPTIONS);
@@ -188,7 +202,7 @@ describe('ChannelSelectionStep', () => {
188
202
  />,
189
203
  );
190
204
 
191
- await userEvent.click(screen.getByRole('button', { name: /content template/i }));
205
+ await userEvent.click(screen.getByRole('button', { name: /add creative/i }));
192
206
  await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
193
207
  await userEvent.click(within(screen.getByRole('menu')).getByText('SMS'));
194
208
  await waitFor(() => expect(screen.getByTestId('creatives-mock')).toBeInTheDocument(), WAIT_OPTIONS);
@@ -236,7 +250,7 @@ describe('ChannelSelectionStep', () => {
236
250
  });
237
251
  });
238
252
 
239
- it('preview menu logs content id', async () => {
253
+ it('preview menu opens test-and-preview slidebox', async () => {
240
254
  renderStep(
241
255
  <ChannelSelectionStep
242
256
  value={{
@@ -253,7 +267,7 @@ describe('ChannelSelectionStep', () => {
253
267
  await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
254
268
  await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
255
269
 
256
- expect(console.log).toHaveBeenCalledWith('Preview content', 'p1');
270
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
257
271
  });
258
272
 
259
273
  it('delivery settings section forwards onDeliverySettingChange to onChange', async () => {
@@ -285,6 +299,7 @@ describe('ChannelSelectionStep', () => {
285
299
  onChange={onChange}
286
300
  channels={CHANNELS}
287
301
  incentivesData={{
302
+ enabled: true,
288
303
  types: [
289
304
  { value: 'badges', label: 'Badges', isActive: true },
290
305
  { value: 'coupons', label: 'Coupons', isActive: true },
@@ -316,6 +331,7 @@ describe('ChannelSelectionStep', () => {
316
331
  onChange={onChange}
317
332
  channels={CHANNELS}
318
333
  incentivesData={{
334
+ enabled: true,
319
335
  types: [{ value: 'badges', label: 'Badges', isActive: true }],
320
336
  }}
321
337
  />,
@@ -342,7 +358,7 @@ describe('ChannelSelectionStep', () => {
342
358
  {
343
359
  contentId: 'mp1',
344
360
  channel: 'MOBILEPUSH',
345
- templateData: { notificationTitle: 'Push title here' },
361
+ templateData: { androidContent: { title: 'Push title here' } },
346
362
  },
347
363
  {
348
364
  contentId: 'em2',
@@ -362,7 +378,7 @@ describe('ChannelSelectionStep', () => {
362
378
  expect(screen.getByText('from@example.com')).toBeInTheDocument();
363
379
  });
364
380
 
365
- it('mobile push preview falls back to notification body when title is absent', () => {
381
+ it('mobile push preview shows message body when title is absent', () => {
366
382
  renderStep(
367
383
  <ChannelSelectionStep
368
384
  value={{
@@ -370,7 +386,7 @@ describe('ChannelSelectionStep', () => {
370
386
  {
371
387
  contentId: 'mp-body',
372
388
  channel: 'MOBILEPUSH',
373
- templateData: { notificationBody: 'Body only preview text' },
389
+ templateData: { androidContent: { message: 'Body only preview text' } },
374
390
  },
375
391
  ],
376
392
  }}
@@ -422,7 +438,7 @@ describe('ChannelSelectionStep', () => {
422
438
  />,
423
439
  );
424
440
 
425
- await userEvent.click(screen.getByRole('button', { name: /content template/i }));
441
+ await userEvent.click(screen.getByRole('button', { name: /add creative/i }));
426
442
  expect(screen.queryByRole('menu')).not.toBeInTheDocument();
427
443
  });
428
444
 
@@ -436,7 +452,7 @@ describe('ChannelSelectionStep', () => {
436
452
  />,
437
453
  );
438
454
 
439
- await userEvent.click(screen.getByRole('button', { name: /content template/i }));
455
+ await userEvent.click(screen.getByRole('button', { name: /add creative/i }));
440
456
  expect(screen.queryByRole('menu')).not.toBeInTheDocument();
441
457
  });
442
458
 
@@ -462,14 +478,14 @@ describe('ChannelSelectionStep', () => {
462
478
  renderStep(
463
479
  <ChannelSelectionStep value={null} onChange={jest.fn()} channels={CHANNELS} />,
464
480
  );
465
- expect(screen.getByText('Add message content and incentive')).toBeInTheDocument();
481
+ expect(screen.getByRole('button', { name: /add creative/i })).toBeInTheDocument();
466
482
  });
467
483
 
468
484
  it('uses CHANNELS fallback when channels prop is null', async () => {
469
485
  renderStep(
470
486
  <ChannelSelectionStep value={{ contentItems: [] }} onChange={jest.fn()} channels={null} />,
471
487
  );
472
- await userEvent.click(screen.getByRole('button', { name: /content template/i }));
488
+ await userEvent.click(screen.getByRole('button', { name: /add creative/i }));
473
489
  await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
474
490
  expect(within(screen.getByRole('menu')).getByText('SMS')).toBeInTheDocument();
475
491
  });
@@ -492,7 +508,7 @@ describe('ChannelSelectionStep', () => {
492
508
  ]}
493
509
  />,
494
510
  );
495
- await userEvent.click(screen.getByRole('button', { name: /content template/i }));
511
+ await userEvent.click(screen.getByRole('button', { name: /add creative/i }));
496
512
  await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
497
513
  await userEvent.click(screen.getByText('Custom'));
498
514
  expect(onChange).toHaveBeenCalledWith({ channel: 'CUSTOM', channels: [] });
@@ -504,7 +520,7 @@ describe('ChannelSelectionStep', () => {
504
520
  renderStep(
505
521
  <ChannelSelectionStep value={{ contentItems: [] }} onChange={onChange} channels={CHANNELS} />,
506
522
  );
507
- await userEvent.click(screen.getByRole('button', { name: /content template/i }));
523
+ await userEvent.click(screen.getByRole('button', { name: /add creative/i }));
508
524
  await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
509
525
  await userEvent.click(within(screen.getByRole('menu')).getByText('SMS'));
510
526
  await waitFor(() => expect(screen.getByTestId('creatives-mock')).toBeInTheDocument(), WAIT_OPTIONS);
@@ -542,14 +558,14 @@ describe('ChannelSelectionStep', () => {
542
558
  value={{ contentItems: [] }}
543
559
  onChange={jest.fn()}
544
560
  channels={CHANNELS}
545
- creativesMode="preview"
561
+ creativesMode="create"
546
562
  />,
547
563
  );
548
- await userEvent.click(screen.getByRole('button', { name: /content template/i }));
564
+ await userEvent.click(screen.getByRole('button', { name: /add creative/i }));
549
565
  await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
550
566
  await userEvent.click(within(screen.getByRole('menu')).getByText('SMS'));
551
567
  await waitFor(() => expect(screen.getByTestId('creatives-mock')).toBeInTheDocument(), WAIT_OPTIONS);
552
- expect(screen.getByTestId('creatives-mock')).toHaveAttribute('data-creatives-mode', 'preview');
568
+ expect(screen.getByTestId('creatives-mock')).toHaveAttribute('data-creatives-mode', 'create');
553
569
  });
554
570
 
555
571
  it('appends to selectedOfferDetails when an incentive is already selected', async () => {
@@ -561,6 +577,7 @@ describe('ChannelSelectionStep', () => {
561
577
  channels={CHANNELS}
562
578
  selectedOfferDetails={[{ type: 'existing' }]}
563
579
  incentivesData={{
580
+ enabled: true,
564
581
  types: [{ value: 'badges', label: 'Badges', isActive: true }],
565
582
  }}
566
583
  />,
@@ -573,12 +590,16 @@ describe('ChannelSelectionStep', () => {
573
590
  });
574
591
  });
575
592
 
576
- it('uses template message field and non-string smsBody fallback in preview', () => {
593
+ it('line message text and non-string smsBody fallback show in preview', () => {
577
594
  renderStep(
578
595
  <ChannelSelectionStep
579
596
  value={{
580
597
  contentItems: [
581
- { contentId: 'line1', channel: 'LINE', templateData: { message: 'Line message preview' } },
598
+ {
599
+ contentId: 'line1',
600
+ channel: 'LINE',
601
+ templateData: { messageBody: JSON.stringify({ messages: [{ text: 'Line message preview' }] }) },
602
+ },
582
603
  { contentId: 'sms-num', channel: 'SMS', templateData: { smsBody: 42 } },
583
604
  ],
584
605
  }}
@@ -598,22 +619,22 @@ describe('ChannelSelectionStep', () => {
598
619
  {
599
620
  contentId: 'mpush1',
600
621
  channel: 'MPUSH',
601
- templateData: { notificationTitle: 'Direct MPUSH title' },
622
+ templateData: { androidContent: { title: 'Direct MPUSH title' } },
602
623
  },
603
624
  {
604
625
  contentId: 'and',
605
626
  channel: 'MOBILEPUSH',
606
- templateData: { androidContent: { notificationTitle: 'Android title' } },
627
+ templateData: { androidContent: { title: 'Android title' } },
607
628
  },
608
629
  {
609
630
  contentId: 'ios',
610
631
  channel: 'MOBILEPUSH',
611
- templateData: { iosContent: { body: 'Ios body text' } },
632
+ templateData: { iosContent: { message: 'Ios body text' } },
612
633
  },
613
634
  {
614
635
  contentId: 'and-body',
615
636
  channel: 'MOBILEPUSH',
616
- templateData: { androidContent: { notificationBody: 'Android notif body' } },
637
+ templateData: { androidContent: { message: 'Android notif body' } },
617
638
  },
618
639
  ],
619
640
  }}
@@ -685,11 +706,11 @@ describe('ChannelSelectionStep', () => {
685
706
  channels={CHANNELS}
686
707
  />,
687
708
  );
688
- expect(screen.getByText('VIBER')).toBeInTheDocument();
709
+ expect(screen.getByLabelText('Show more content options icon')).toBeInTheDocument();
689
710
  parseSpy.mockRestore();
690
711
  });
691
712
 
692
- it('Viber preview falls back to channel when parsed JSON has no text (empty messageBody)', () => {
713
+ it('Viber preview renders card when parsed JSON has no text (empty messageBody)', () => {
693
714
  renderStep(
694
715
  <ChannelSelectionStep
695
716
  value={{
@@ -705,7 +726,7 @@ describe('ChannelSelectionStep', () => {
705
726
  channels={CHANNELS}
706
727
  />,
707
728
  );
708
- expect(screen.getByText('VIBER')).toBeInTheDocument();
729
+ expect(screen.getByLabelText('Show more content options icon')).toBeInTheDocument();
709
730
  });
710
731
 
711
732
  it('does not open creatives when clicked channel is in channelsToDisable', async () => {
@@ -719,7 +740,7 @@ describe('ChannelSelectionStep', () => {
719
740
  channelsToDisable={['SMS']}
720
741
  />,
721
742
  );
722
- await userEvent.click(screen.getByRole('button', { name: /content template/i }));
743
+ await userEvent.click(screen.getByRole('button', { name: /add creative/i }));
723
744
  await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
724
745
  // SMS is in channelsToDisable — clicking it should not call onChange or show creatives
725
746
  await userEvent.click(within(screen.getByRole('menu')).getByText('SMS'));
@@ -774,7 +795,7 @@ describe('ChannelSelectionStep', () => {
774
795
  channelsToDisable={['EMAIL']}
775
796
  />,
776
797
  );
777
- await userEvent.click(screen.getByRole('button', { name: /content template/i }));
798
+ await userEvent.click(screen.getByRole('button', { name: /add creative/i }));
778
799
  await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
779
800
  // Click the disabled EMAIL menu item — handleChannelSelect should return early
780
801
  await userEvent.click(within(screen.getByRole('menu')).getByText('Email'));
@@ -782,8 +803,7 @@ describe('ChannelSelectionStep', () => {
782
803
  expect(screen.queryByTestId('creatives-mock')).not.toBeInTheDocument();
783
804
  });
784
805
 
785
- it('ios notification body shows as preview when iosContent.notificationBody is present', () => {
786
- // Covers line 125: templateData.iosContent.notificationBody fallback branch
806
+ it('ios notification body shows as preview when iosContent.message is present', () => {
787
807
  renderStep(
788
808
  <ChannelSelectionStep
789
809
  value={{
@@ -791,7 +811,7 @@ describe('ChannelSelectionStep', () => {
791
811
  {
792
812
  contentId: 'ios-nb',
793
813
  channel: 'MOBILEPUSH',
794
- templateData: { iosContent: { notificationBody: 'IOS notif body text' } },
814
+ templateData: { iosContent: { message: 'IOS notif body text' } },
795
815
  },
796
816
  ],
797
817
  }}
@@ -801,4 +821,1096 @@ describe('ChannelSelectionStep', () => {
801
821
  );
802
822
  expect(screen.getByText(/IOS notif body text/)).toBeInTheDocument();
803
823
  });
824
+
825
+ // ── handleCloseTestAndPreview ────────────────────────────────────────────────
826
+
827
+ it('closing TestAndPreview via onClose hides the slidebox', async () => {
828
+ renderStep(
829
+ <ChannelSelectionStep
830
+ value={{
831
+ contentItems: [{ contentId: 'c-close', channel: 'SMS', templateData: { smsBody: 'x' } }],
832
+ }}
833
+ onChange={jest.fn()}
834
+ channels={CHANNELS}
835
+ />,
836
+ );
837
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
838
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
839
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
840
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
841
+
842
+ await userEvent.click(screen.getByTestId('tap-close'));
843
+
844
+ expect(screen.queryByTestId('test-and-preview-mock')).not.toBeInTheDocument();
845
+ });
846
+
847
+ // ── PREVIEW_TEST_UNSUPPORTED_CHANNELS ────────────────────────────────────────
848
+
849
+ it.each(['LINE', 'FACEBOOK', 'CALLTASK'])(
850
+ '%s content card omits the "Preview and Test" menu item',
851
+ async (channel) => {
852
+ renderStep(
853
+ <ChannelSelectionStep
854
+ value={{
855
+ contentItems: [{ contentId: 'unsupported-1', channel, templateData: { messageBody: 'body' } }],
856
+ }}
857
+ onChange={jest.fn()}
858
+ channels={CHANNELS}
859
+ />,
860
+ );
861
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
862
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
863
+ expect(within(screen.getByRole('menu')).queryByText('Preview and Test')).not.toBeInTheDocument();
864
+ },
865
+ );
866
+
867
+ // ── extractContentForPreview — EMAIL ─────────────────────────────────────────
868
+
869
+ it('EMAIL preview passes EMAIL channel to TestAndPreview slidebox', async () => {
870
+ renderStep(
871
+ <ChannelSelectionStep
872
+ value={{
873
+ contentItems: [{
874
+ contentId: 'ep1',
875
+ channel: 'EMAIL',
876
+ templateData: { emailSubject: 'Hello', emailBody: '<p>Body</p>' },
877
+ }],
878
+ }}
879
+ onChange={jest.fn()}
880
+ channels={CHANNELS}
881
+ />,
882
+ );
883
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
884
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
885
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
886
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
887
+ expect(screen.getByTestId('test-and-preview-mock')).toHaveAttribute('data-channel', 'EMAIL');
888
+ });
889
+
890
+ it('EMAIL preview uses emailHtml when emailBody is absent', async () => {
891
+ renderStep(
892
+ <ChannelSelectionStep
893
+ value={{
894
+ contentItems: [{
895
+ contentId: 'ep2',
896
+ channel: 'EMAIL',
897
+ templateData: { emailHtml: '<b>HTML</b>' },
898
+ }],
899
+ }}
900
+ onChange={jest.fn()}
901
+ channels={CHANNELS}
902
+ />,
903
+ );
904
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
905
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
906
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
907
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
908
+ });
909
+
910
+ // ── extractContentForPreview — MOBILEPUSH / MPUSH ────────────────────────────
911
+
912
+ it('MOBILEPUSH preview passes MOBILEPUSH channel and handles expandableDetails optional chaining', async () => {
913
+ renderStep(
914
+ <ChannelSelectionStep
915
+ value={{
916
+ contentItems: [{
917
+ contentId: 'mp-ep',
918
+ channel: 'MOBILEPUSH',
919
+ templateData: {
920
+ accountId: 'acc1',
921
+ androidContent: {
922
+ title: 'And title',
923
+ message: 'And msg',
924
+ expandableDetails: {
925
+ image: 'http://img.png',
926
+ media: ['http://vid.mp4'],
927
+ ctas: [{ label: 'Click', actionLink: 'http://link' }],
928
+ carouselData: [{ id: 1 }],
929
+ },
930
+ },
931
+ iosContent: { title: 'iOS title', message: 'iOS msg' },
932
+ },
933
+ }],
934
+ }}
935
+ onChange={jest.fn()}
936
+ channels={CHANNELS}
937
+ />,
938
+ );
939
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
940
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
941
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
942
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
943
+ expect(screen.getByTestId('test-and-preview-mock')).toHaveAttribute('data-channel', 'MOBILEPUSH');
944
+ });
945
+
946
+ it('MOBILEPUSH preview with no expandableDetails uses fallback empty values', async () => {
947
+ renderStep(
948
+ <ChannelSelectionStep
949
+ value={{
950
+ contentItems: [{
951
+ contentId: 'mp-no-exp',
952
+ channel: 'MOBILEPUSH',
953
+ templateData: { androidContent: {}, iosContent: {} },
954
+ }],
955
+ }}
956
+ onChange={jest.fn()}
957
+ channels={CHANNELS}
958
+ />,
959
+ );
960
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
961
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
962
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
963
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
964
+ });
965
+
966
+ it('MPUSH preview hits the MOBILEPUSH branch (same code path)', async () => {
967
+ renderStep(
968
+ <ChannelSelectionStep
969
+ value={{
970
+ contentItems: [{
971
+ contentId: 'mpush-ep',
972
+ channel: 'MPUSH',
973
+ templateData: { androidContent: { title: 'MPUSH title' } },
974
+ }],
975
+ }}
976
+ onChange={jest.fn()}
977
+ channels={CHANNELS}
978
+ />,
979
+ );
980
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
981
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
982
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
983
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
984
+ expect(screen.getByTestId('test-and-preview-mock')).toHaveAttribute('data-channel', 'MPUSH');
985
+ });
986
+
987
+ // ── extractContentForPreview — INAPP ─────────────────────────────────────────
988
+
989
+ it('INAPP preview with image sets templateMediaType IMAGE', async () => {
990
+ renderStep(
991
+ <ChannelSelectionStep
992
+ value={{
993
+ contentItems: [{
994
+ contentId: 'inapp-img',
995
+ channel: 'INAPP',
996
+ templateData: {
997
+ accountId: 'acc-inapp',
998
+ androidContent: {
999
+ title: 'InApp title',
1000
+ message: 'InApp msg',
1001
+ bodyType: 'BANNER',
1002
+ expandableDetails: {
1003
+ image: 'http://inapp-img.png',
1004
+ ctas: [{ label: 'Go', actionLink: 'http://deep' }],
1005
+ },
1006
+ },
1007
+ iosContent: {
1008
+ expandableDetails: { image: 'http://ios-img.png' },
1009
+ },
1010
+ },
1011
+ }],
1012
+ }}
1013
+ onChange={jest.fn()}
1014
+ channels={CHANNELS}
1015
+ />,
1016
+ );
1017
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1018
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1019
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
1020
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1021
+ expect(screen.getByTestId('test-and-preview-mock')).toHaveAttribute('data-channel', 'INAPP');
1022
+ });
1023
+
1024
+ it('INAPP preview without image sets templateMediaType TEXT and deepLinkValue null when no CTAs', async () => {
1025
+ renderStep(
1026
+ <ChannelSelectionStep
1027
+ value={{
1028
+ contentItems: [{
1029
+ contentId: 'inapp-noimg',
1030
+ channel: 'INAPP',
1031
+ templateData: {
1032
+ androidContent: { title: 'Title', expandableDetails: { ctas: [] } },
1033
+ iosContent: {},
1034
+ },
1035
+ }],
1036
+ }}
1037
+ onChange={jest.fn()}
1038
+ channels={CHANNELS}
1039
+ />,
1040
+ );
1041
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1042
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1043
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
1044
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1045
+ });
1046
+
1047
+ // ── extractContentForPreview — WHATSAPP ──────────────────────────────────────
1048
+
1049
+ it('WHATSAPP preview with IMAGE mediaType populates whatsappImageSrc', async () => {
1050
+ renderStep(
1051
+ <ChannelSelectionStep
1052
+ value={{
1053
+ contentItems: [{
1054
+ contentId: 'wa-img',
1055
+ channel: 'WHATSAPP',
1056
+ templateData: {
1057
+ messageBody: 'WA body',
1058
+ accountId: 'wa-acc',
1059
+ sourceAccountIdentifier: 'wa-src',
1060
+ accountName: 'WA Name',
1061
+ templateConfigs: {
1062
+ mediaType: 'IMAGE',
1063
+ whatsappMedia: { url: 'http://img.png', header: 'Header', footer: 'Footer' },
1064
+ cards: [],
1065
+ },
1066
+ },
1067
+ }],
1068
+ }}
1069
+ onChange={jest.fn()}
1070
+ channels={CHANNELS}
1071
+ />,
1072
+ );
1073
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1074
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1075
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
1076
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1077
+ expect(screen.getByTestId('test-and-preview-mock')).toHaveAttribute('data-channel', 'WHATSAPP');
1078
+ });
1079
+
1080
+ it('WHATSAPP preview with VIDEO mediaType populates whatsappVideoPreviewImg', async () => {
1081
+ renderStep(
1082
+ <ChannelSelectionStep
1083
+ value={{
1084
+ contentItems: [{
1085
+ contentId: 'wa-vid',
1086
+ channel: 'WHATSAPP',
1087
+ templateData: {
1088
+ messageBody: 'WA video',
1089
+ templateConfigs: {
1090
+ mediaType: 'VIDEO',
1091
+ whatsappMedia: { previewUrl: 'http://thumb.png' },
1092
+ },
1093
+ },
1094
+ }],
1095
+ }}
1096
+ onChange={jest.fn()}
1097
+ channels={CHANNELS}
1098
+ />,
1099
+ );
1100
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1101
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1102
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
1103
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1104
+ });
1105
+
1106
+ it('WHATSAPP preview with DOCUMENT + docParams calls getWhatsappDocPreview', async () => {
1107
+ renderStep(
1108
+ <ChannelSelectionStep
1109
+ value={{
1110
+ contentItems: [{
1111
+ contentId: 'wa-doc',
1112
+ channel: 'WHATSAPP',
1113
+ templateData: {
1114
+ templateConfigs: {
1115
+ mediaType: 'DOCUMENT',
1116
+ whatsappMedia: {
1117
+ previewUrl: 'http://doc-thumb.png',
1118
+ docParams: { fileName: 'file.pdf', fileSize: 100 },
1119
+ },
1120
+ },
1121
+ },
1122
+ }],
1123
+ }}
1124
+ onChange={jest.fn()}
1125
+ channels={CHANNELS}
1126
+ />,
1127
+ );
1128
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1129
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1130
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
1131
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1132
+ });
1133
+
1134
+ it('WHATSAPP preview with DOCUMENT without docParams sets docPreview null', async () => {
1135
+ renderStep(
1136
+ <ChannelSelectionStep
1137
+ value={{
1138
+ contentItems: [{
1139
+ contentId: 'wa-doc-null',
1140
+ channel: 'WHATSAPP',
1141
+ templateData: {
1142
+ templateConfigs: {
1143
+ mediaType: 'DOCUMENT',
1144
+ whatsappMedia: {},
1145
+ },
1146
+ },
1147
+ }],
1148
+ }}
1149
+ onChange={jest.fn()}
1150
+ channels={CHANNELS}
1151
+ />,
1152
+ );
1153
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1154
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1155
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
1156
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1157
+ });
1158
+
1159
+ it('WHATSAPP preview with CTA buttons populates ctaData', async () => {
1160
+ renderStep(
1161
+ <ChannelSelectionStep
1162
+ value={{
1163
+ contentItems: [{
1164
+ contentId: 'wa-cta',
1165
+ channel: 'WHATSAPP',
1166
+ templateData: {
1167
+ templateConfigs: {
1168
+ buttonType: 'CTA',
1169
+ buttons: [{ label: 'Buy', url: 'http://buy' }],
1170
+ },
1171
+ },
1172
+ }],
1173
+ }}
1174
+ onChange={jest.fn()}
1175
+ channels={CHANNELS}
1176
+ />,
1177
+ );
1178
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1179
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1180
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
1181
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1182
+ });
1183
+
1184
+ it('WHATSAPP preview with QUICK_REPLY buttons populates quickReplyData', async () => {
1185
+ renderStep(
1186
+ <ChannelSelectionStep
1187
+ value={{
1188
+ contentItems: [{
1189
+ contentId: 'wa-qr',
1190
+ channel: 'WHATSAPP',
1191
+ templateData: {
1192
+ templateConfigs: {
1193
+ buttonType: 'QUICK_REPLY',
1194
+ buttons: [{ text: 'Yes' }, { text: 'No' }],
1195
+ cards: [{ id: 'card1' }],
1196
+ },
1197
+ },
1198
+ }],
1199
+ }}
1200
+ onChange={jest.fn()}
1201
+ channels={CHANNELS}
1202
+ />,
1203
+ );
1204
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1205
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1206
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
1207
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1208
+ });
1209
+
1210
+ // ── extractContentForPreview — VIBER ─────────────────────────────────────────
1211
+
1212
+ it('VIBER preview with string messageBody containing video and accountDetails object', async () => {
1213
+ const messageBody = JSON.stringify({
1214
+ content: {
1215
+ text: 'Hello from Viber',
1216
+ image: { url: 'http://img.png' },
1217
+ button: { text: 'Click' },
1218
+ video: { url: 'http://vid.mp4', thumbnailUrl: 'http://thumb.jpg' },
1219
+ },
1220
+ });
1221
+ renderStep(
1222
+ <ChannelSelectionStep
1223
+ value={{
1224
+ contentItems: [{
1225
+ contentId: 'vib-full',
1226
+ channel: 'VIBER',
1227
+ templateData: {
1228
+ messageBody,
1229
+ accountDetails: { name: 'My Brand', accountName: 'fallback' },
1230
+ },
1231
+ }],
1232
+ }}
1233
+ onChange={jest.fn()}
1234
+ channels={CHANNELS}
1235
+ />,
1236
+ );
1237
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1238
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1239
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
1240
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1241
+ expect(screen.getByTestId('test-and-preview-mock')).toHaveAttribute('data-channel', 'VIBER');
1242
+ });
1243
+
1244
+ it('VIBER preview with string accountDetails', async () => {
1245
+ renderStep(
1246
+ <ChannelSelectionStep
1247
+ value={{
1248
+ contentItems: [{
1249
+ contentId: 'vib-str-acct',
1250
+ channel: 'VIBER',
1251
+ templateData: {
1252
+ messageBody: JSON.stringify({ content: { text: 'Hi' } }),
1253
+ accountDetails: JSON.stringify({ accountName: 'Brand Name' }),
1254
+ },
1255
+ }],
1256
+ }}
1257
+ onChange={jest.fn()}
1258
+ channels={CHANNELS}
1259
+ />,
1260
+ );
1261
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1262
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1263
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
1264
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1265
+ });
1266
+
1267
+ it('VIBER preview with object messageBody (non-string) uses it directly', async () => {
1268
+ renderStep(
1269
+ <ChannelSelectionStep
1270
+ value={{
1271
+ contentItems: [{
1272
+ contentId: 'vib-obj',
1273
+ channel: 'VIBER',
1274
+ templateData: {
1275
+ messageBody: { content: { text: 'Obj message' } },
1276
+ accountDetails: {},
1277
+ },
1278
+ }],
1279
+ }}
1280
+ onChange={jest.fn()}
1281
+ channels={CHANNELS}
1282
+ />,
1283
+ );
1284
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1285
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1286
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
1287
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1288
+ });
1289
+
1290
+ it('VIBER preview: messageBody JSON.parse throws falls back to empty object', async () => {
1291
+ const spy = jest.spyOn(JSON, 'parse').mockImplementationOnce(() => {
1292
+ throw new SyntaxError('bad json');
1293
+ });
1294
+ renderStep(
1295
+ <ChannelSelectionStep
1296
+ value={{
1297
+ contentItems: [{
1298
+ contentId: 'vib-throws',
1299
+ channel: 'VIBER',
1300
+ templateData: { messageBody: 'invalid json', accountDetails: {} },
1301
+ }],
1302
+ }}
1303
+ onChange={jest.fn()}
1304
+ channels={CHANNELS}
1305
+ />,
1306
+ );
1307
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1308
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1309
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
1310
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1311
+ spy.mockRestore();
1312
+ });
1313
+
1314
+ it('VIBER preview: accountDetails JSON.parse throws falls back to empty object', async () => {
1315
+ const realParse = JSON.parse;
1316
+ let callCount = 0;
1317
+ const spy = jest.spyOn(JSON, 'parse').mockImplementation((...args) => {
1318
+ callCount += 1;
1319
+ // Let messageBody parse succeed, fail on accountDetails parse (second call)
1320
+ if (callCount === 2) throw new SyntaxError('bad acct json');
1321
+ return realParse.apply(JSON, args);
1322
+ });
1323
+ renderStep(
1324
+ <ChannelSelectionStep
1325
+ value={{
1326
+ contentItems: [{
1327
+ contentId: 'vib-acct-throws',
1328
+ channel: 'VIBER',
1329
+ templateData: {
1330
+ messageBody: JSON.stringify({ content: { text: 'ok' } }),
1331
+ accountDetails: 'bad-json',
1332
+ },
1333
+ }],
1334
+ }}
1335
+ onChange={jest.fn()}
1336
+ channels={CHANNELS}
1337
+ />,
1338
+ );
1339
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1340
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1341
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
1342
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1343
+ spy.mockRestore();
1344
+ });
1345
+
1346
+ it('VIBER preview with no content wrapper uses parsedBody directly', async () => {
1347
+ renderStep(
1348
+ <ChannelSelectionStep
1349
+ value={{
1350
+ contentItems: [{
1351
+ contentId: 'vib-nokey',
1352
+ channel: 'VIBER',
1353
+ templateData: {
1354
+ messageBody: JSON.stringify({ text: 'Flat text', image: { url: 'http://img' } }),
1355
+ },
1356
+ }],
1357
+ }}
1358
+ onChange={jest.fn()}
1359
+ channels={CHANNELS}
1360
+ />,
1361
+ );
1362
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1363
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1364
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
1365
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1366
+ });
1367
+
1368
+ // ── extractContentForPreview — RCS ───────────────────────────────────────────
1369
+
1370
+ it('RCS preview with IMAGE media type renders without error', async () => {
1371
+ renderStep(
1372
+ <ChannelSelectionStep
1373
+ value={{
1374
+ contentItems: [{
1375
+ contentId: 'rcs-img',
1376
+ channel: 'RCS',
1377
+ templateData: {
1378
+ rcsContent: {
1379
+ cardContent: [{
1380
+ title: 'RCS title',
1381
+ description: 'RCS desc',
1382
+ media: { mediaType: 'IMAGE', mediaUrl: 'http://rcs-img.png' },
1383
+ suggestions: [{ text: 'Reply1' }],
1384
+ }],
1385
+ },
1386
+ },
1387
+ }],
1388
+ }}
1389
+ onChange={jest.fn()}
1390
+ channels={CHANNELS}
1391
+ />,
1392
+ );
1393
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1394
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1395
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
1396
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1397
+ expect(screen.getByTestId('test-and-preview-mock')).toHaveAttribute('data-channel', 'RCS');
1398
+ });
1399
+
1400
+ it('RCS preview with VIDEO media type includes rcsThumbnailSrc', async () => {
1401
+ renderStep(
1402
+ <ChannelSelectionStep
1403
+ value={{
1404
+ contentItems: [{
1405
+ contentId: 'rcs-vid',
1406
+ channel: 'RCS',
1407
+ templateData: {
1408
+ rcsContent: {
1409
+ cardContent: [{
1410
+ media: { mediaType: 'VIDEO', mediaUrl: 'http://vid.mp4', thumbnailUrl: 'http://thumb.jpg' },
1411
+ suggestions: [],
1412
+ }],
1413
+ },
1414
+ },
1415
+ }],
1416
+ }}
1417
+ onChange={jest.fn()}
1418
+ channels={CHANNELS}
1419
+ />,
1420
+ );
1421
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1422
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1423
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
1424
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1425
+ });
1426
+
1427
+ it('RCS preview with no rcsContent uses empty fallbacks', async () => {
1428
+ renderStep(
1429
+ <ChannelSelectionStep
1430
+ value={{
1431
+ contentItems: [{ contentId: 'rcs-empty', channel: 'RCS', templateData: {} }],
1432
+ }}
1433
+ onChange={jest.fn()}
1434
+ channels={CHANNELS}
1435
+ />,
1436
+ );
1437
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1438
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1439
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
1440
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1441
+ });
1442
+
1443
+ // ── extractContentForPreview — WEBPUSH ───────────────────────────────────────
1444
+
1445
+ it('WEBPUSH preview passes messageContent.content to slidebox', async () => {
1446
+ renderStep(
1447
+ <ChannelSelectionStep
1448
+ value={{
1449
+ contentItems: [{
1450
+ contentId: 'wp-1',
1451
+ channel: 'WEBPUSH',
1452
+ templateData: {
1453
+ messageContent: {
1454
+ content: { title: 'WP title', message: 'WP message', icon: 'http://icon.png' },
1455
+ },
1456
+ },
1457
+ }],
1458
+ }}
1459
+ onChange={jest.fn()}
1460
+ channels={CHANNELS}
1461
+ />,
1462
+ );
1463
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1464
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1465
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
1466
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1467
+ expect(screen.getByTestId('test-and-preview-mock')).toHaveAttribute('data-channel', 'WEBPUSH');
1468
+ });
1469
+
1470
+ it('WEBPUSH preview with missing messageContent uses empty object fallback', async () => {
1471
+ renderStep(
1472
+ <ChannelSelectionStep
1473
+ value={{
1474
+ contentItems: [{ contentId: 'wp-empty', channel: 'WEBPUSH', templateData: {} }],
1475
+ }}
1476
+ onChange={jest.fn()}
1477
+ channels={CHANNELS}
1478
+ />,
1479
+ );
1480
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1481
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1482
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
1483
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1484
+ });
1485
+
1486
+ // ── extractContentForPreview — ZALO ──────────────────────────────────────────
1487
+
1488
+ it('ZALO preview uses templateConfigs.template as templatePreviewUrl', async () => {
1489
+ renderStep(
1490
+ <ChannelSelectionStep
1491
+ value={{
1492
+ contentItems: [{
1493
+ contentId: 'zalo-1',
1494
+ channel: 'ZALO',
1495
+ templateData: {
1496
+ templateConfigs: { template: 'http://zalo-template.json' },
1497
+ templatePreviewUrl: 'http://fallback.json',
1498
+ },
1499
+ }],
1500
+ }}
1501
+ onChange={jest.fn()}
1502
+ channels={CHANNELS}
1503
+ />,
1504
+ );
1505
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1506
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1507
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
1508
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1509
+ expect(screen.getByTestId('test-and-preview-mock')).toHaveAttribute('data-channel', 'ZALO');
1510
+ });
1511
+
1512
+ it('ZALO preview falls back to templatePreviewUrl when templateConfigs.template is absent', async () => {
1513
+ renderStep(
1514
+ <ChannelSelectionStep
1515
+ value={{
1516
+ contentItems: [{
1517
+ contentId: 'zalo-2',
1518
+ channel: 'ZALO',
1519
+ templateData: { templatePreviewUrl: 'http://preview.json' },
1520
+ }],
1521
+ }}
1522
+ onChange={jest.fn()}
1523
+ channels={CHANNELS}
1524
+ />,
1525
+ );
1526
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1527
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1528
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
1529
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1530
+ });
1531
+
1532
+ // ── extractContentForPreview — fallback (Facebook) ───────────────────────────
1533
+
1534
+ it('fallback: unmapped channel (not in any extractContentForPreview branch) passes templateData directly', async () => {
1535
+ // 'TWITTER' is not in PREVIEW_TEST_UNSUPPORTED_CHANNELS → "Preview and Test" shows.
1536
+ // 'TWITTER' also doesn't match any if-branch in extractContentForPreview → hits line 352 fallback.
1537
+ renderStep(
1538
+ <ChannelSelectionStep
1539
+ value={{
1540
+ contentItems: [{
1541
+ contentId: 'fallback-twitter',
1542
+ channel: 'TWITTER',
1543
+ templateData: { messageBody: 'Fallback content' },
1544
+ }],
1545
+ }}
1546
+ onChange={jest.fn()}
1547
+ channels={CHANNELS}
1548
+ />,
1549
+ );
1550
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1551
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1552
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
1553
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1554
+ });
1555
+
1556
+ // ── extractContentForPreview — SMS via messageBody ────────────────────────────
1557
+
1558
+ it('SMS preview uses messageBody when smsBody is absent', async () => {
1559
+ renderStep(
1560
+ <ChannelSelectionStep
1561
+ value={{
1562
+ contentItems: [{
1563
+ contentId: 'sms-mb',
1564
+ channel: 'SMS',
1565
+ templateData: { messageBody: 'From messageBody' },
1566
+ }],
1567
+ }}
1568
+ onChange={jest.fn()}
1569
+ channels={CHANNELS}
1570
+ />,
1571
+ );
1572
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1573
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1574
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
1575
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1576
+ expect(screen.getByTestId('test-and-preview-mock')).toHaveAttribute('data-channel', 'SMS');
1577
+ });
1578
+
1579
+ // ── getCardType — INAPP and WECHAT return undefined ─────────────────────────
1580
+
1581
+ it('INAPP content card renders without crash (getCardType returns undefined for INAPP)', () => {
1582
+ renderStep(
1583
+ <ChannelSelectionStep
1584
+ value={{
1585
+ contentItems: [{
1586
+ contentId: 'inapp-card',
1587
+ channel: 'INAPP',
1588
+ templateData: { androidContent: { title: 'InApp Card Title' } },
1589
+ }],
1590
+ }}
1591
+ onChange={jest.fn()}
1592
+ channels={CHANNELS}
1593
+ />,
1594
+ );
1595
+ expect(screen.getByLabelText('Show more content options icon')).toBeInTheDocument();
1596
+ });
1597
+
1598
+ it('WECHAT content card renders without crash (getCardType returns undefined for WECHAT)', () => {
1599
+ renderStep(
1600
+ <ChannelSelectionStep
1601
+ value={{
1602
+ contentItems: [{
1603
+ contentId: 'wechat-card',
1604
+ channel: 'WECHAT',
1605
+ templateData: { messageBody: 'WeChat message' },
1606
+ }],
1607
+ }}
1608
+ onChange={jest.fn()}
1609
+ channels={CHANNELS}
1610
+ />,
1611
+ );
1612
+ expect(screen.getByLabelText('Show more content options icon')).toBeInTheDocument();
1613
+ });
1614
+
1615
+ // ── is_drag_drop → BEE editor ────────────────────────────────────────────────
1616
+
1617
+ it('edit content with is_drag_drop=1 passes BEE as editor to CreativesContainer', async () => {
1618
+ renderStep(
1619
+ <ChannelSelectionStep
1620
+ value={{
1621
+ contentItems: [{
1622
+ contentId: 'bee-item',
1623
+ channel: 'EMAIL',
1624
+ templateData: { emailSubject: 'BEE subject', is_drag_drop: 1 },
1625
+ }],
1626
+ }}
1627
+ onChange={jest.fn()}
1628
+ channels={CHANNELS}
1629
+ />,
1630
+ );
1631
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1632
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1633
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Edit'));
1634
+ await waitFor(() => expect(screen.getByTestId('creatives-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1635
+ });
1636
+
1637
+ it('edit content with is_drag_drop=true also passes BEE editor', async () => {
1638
+ renderStep(
1639
+ <ChannelSelectionStep
1640
+ value={{
1641
+ contentItems: [{
1642
+ contentId: 'bee-bool',
1643
+ channel: 'EMAIL',
1644
+ templateData: { emailSubject: 'BEE bool', is_drag_drop: true },
1645
+ }],
1646
+ }}
1647
+ onChange={jest.fn()}
1648
+ channels={CHANNELS}
1649
+ />,
1650
+ );
1651
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1652
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1653
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Edit'));
1654
+ await waitFor(() => expect(screen.getByTestId('creatives-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1655
+ });
1656
+
1657
+ // ── WEBPUSH edit: messageContent.content extraction ──────────────────────────
1658
+
1659
+ it('edit WEBPUSH item extracts messageContent.content for CreativesContainer templateData', async () => {
1660
+ renderStep(
1661
+ <ChannelSelectionStep
1662
+ value={{
1663
+ contentItems: [{
1664
+ contentId: 'wp-edit',
1665
+ // templateData has a `channel` field so the WEBPUSH branch in the edit IIFE fires
1666
+ channel: 'WEBPUSH',
1667
+ templateData: {
1668
+ channel: 'WEBPUSH',
1669
+ messageContent: {
1670
+ content: { title: 'WP Edit Title', message: 'WP Edit Msg' },
1671
+ },
1672
+ },
1673
+ }],
1674
+ }}
1675
+ onChange={jest.fn()}
1676
+ channels={CHANNELS}
1677
+ />,
1678
+ );
1679
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1680
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1681
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Edit'));
1682
+ await waitFor(() => expect(screen.getByTestId('creatives-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1683
+ expect(screen.getByTestId('creatives-mock')).toHaveAttribute('data-creatives-mode', 'edit');
1684
+ });
1685
+
1686
+ // ── FTP channel filtered from dropdown ───────────────────────────────────────
1687
+
1688
+ it('FTP channel is filtered out of the channel dropdown', async () => {
1689
+ renderStep(
1690
+ <ChannelSelectionStep
1691
+ value={{ contentItems: [] }}
1692
+ onChange={jest.fn()}
1693
+ channels={[
1694
+ {
1695
+ value: 'SMS', label: 'SMS', iconType: 'sms', isActive: true, paneKey: 'sms', channelProp: 'sms',
1696
+ },
1697
+ {
1698
+ value: 'ftp', label: 'FTP', iconType: 'sms', isActive: true, paneKey: 'ftp', channelProp: 'ftp',
1699
+ },
1700
+ ]}
1701
+ />,
1702
+ );
1703
+ await userEvent.click(screen.getByRole('button', { name: /add creative/i }));
1704
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1705
+ expect(within(screen.getByRole('menu')).getByText('SMS')).toBeInTheDocument();
1706
+ expect(within(screen.getByRole('menu')).queryByText('FTP')).not.toBeInTheDocument();
1707
+ });
1708
+
1709
+ // ── getSenderDetailsDisplay returns null for other channels ──────────────────
1710
+
1711
+ it('WHATSAPP content card has no sender details row (getSenderDetailsDisplay returns null)', () => {
1712
+ renderStep(
1713
+ <ChannelSelectionStep
1714
+ value={{
1715
+ contentItems: [{
1716
+ contentId: 'wa-no-sender',
1717
+ channel: 'WHATSAPP',
1718
+ templateData: { messageBody: 'WA msg', templateConfigs: {} },
1719
+ }],
1720
+ }}
1721
+ onChange={jest.fn()}
1722
+ channels={CHANNELS}
1723
+ />,
1724
+ );
1725
+ expect(screen.queryByText(/sender ID/i)).not.toBeInTheDocument();
1726
+ });
1727
+
1728
+ // ── incentive coupons/points keys are skipped ────────────────────────────────
1729
+
1730
+ it('selecting a coupons incentive type from the menu is silently skipped', async () => {
1731
+ const onChange = jest.fn();
1732
+ renderStep(
1733
+ <ChannelSelectionStep
1734
+ value={{ contentItems: [] }}
1735
+ onChange={onChange}
1736
+ channels={CHANNELS}
1737
+ incentivesData={{
1738
+ enabled: true,
1739
+ types: [
1740
+ { value: 'coupons', label: 'Coupons', isActive: true },
1741
+ { value: 'badges', label: 'Badges', isActive: true },
1742
+ ],
1743
+ }}
1744
+ />,
1745
+ );
1746
+ // Only non-coupons/non-points items appear in the dropdown; selecting coupons is impossible
1747
+ // through the rendered dropdown since it's filtered. Verify badges still works.
1748
+ await userEvent.click(screen.getByRole('button', { name: /^add incentive$/i }));
1749
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1750
+ expect(within(screen.getByRole('menu')).queryByText('Coupons')).not.toBeInTheDocument();
1751
+ expect(within(screen.getByRole('menu')).getByText('Badges')).toBeInTheDocument();
1752
+ });
1753
+
1754
+ it('incentive dropdown on card: selecting points type key is skipped (filtered from menu)', async () => {
1755
+ const onChange = jest.fn();
1756
+ renderStep(
1757
+ <ChannelSelectionStep
1758
+ value={{
1759
+ contentItems: [{ contentId: 'c1', channel: 'SMS', templateData: {} }],
1760
+ }}
1761
+ onChange={onChange}
1762
+ channels={CHANNELS}
1763
+ incentivesData={{
1764
+ enabled: true,
1765
+ types: [
1766
+ { value: 'points', label: 'Points', isActive: true },
1767
+ { value: 'promotions', label: 'Promotions', isActive: true },
1768
+ ],
1769
+ }}
1770
+ />,
1771
+ );
1772
+ const addIncentiveLinks = screen.getAllByRole('button', { name: /add incentive/i });
1773
+ await userEvent.click(addIncentiveLinks[addIncentiveLinks.length - 1]);
1774
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1775
+ expect(within(screen.getByRole('menu')).queryByText('Points')).not.toBeInTheDocument();
1776
+ expect(within(screen.getByRole('menu')).getByText('Promotions')).toBeInTheDocument();
1777
+ });
1778
+
1779
+ // ── Viber getContentPreview with object messageBody (non-string) ──────────────
1780
+
1781
+ it('Viber card preview text uses parsed content.text when messageBody is a non-string object', () => {
1782
+ renderStep(
1783
+ <ChannelSelectionStep
1784
+ value={{
1785
+ contentItems: [{
1786
+ contentId: 'vib-obj-preview',
1787
+ channel: 'VIBER',
1788
+ templateData: { messageBody: { content: { text: 'Obj viber text for preview' } } },
1789
+ }],
1790
+ }}
1791
+ onChange={jest.fn()}
1792
+ channels={CHANNELS}
1793
+ />,
1794
+ );
1795
+ // getContentPreview for VIBER parses the object messageBody to extract content.text
1796
+ expect(screen.getByLabelText('Show more content options icon')).toBeInTheDocument();
1797
+ });
1798
+
1799
+ // ── selectedChannelConfig derived from CHANNELS ───────────────────────────────
1800
+
1801
+ it('opening creatives for EMAIL derives correct channelProp from CHANNELS', async () => {
1802
+ const onChange = jest.fn();
1803
+ renderStep(
1804
+ <ChannelSelectionStep
1805
+ value={{ contentItems: [] }}
1806
+ onChange={onChange}
1807
+ channels={CHANNELS}
1808
+ />,
1809
+ );
1810
+ await userEvent.click(screen.getByRole('button', { name: /add creative/i }));
1811
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1812
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Email'));
1813
+ await waitFor(() => expect(screen.getByTestId('creatives-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1814
+ expect(screen.getByTestId('creatives-mock')).toHaveAttribute('data-creatives-channel', 'email');
1815
+ });
1816
+
1817
+ it('opening creatives for MOBILEPUSH derives correct channelProp', async () => {
1818
+ const onChange = jest.fn();
1819
+ renderStep(
1820
+ <ChannelSelectionStep
1821
+ value={{ contentItems: [] }}
1822
+ onChange={onChange}
1823
+ channels={CHANNELS}
1824
+ />,
1825
+ );
1826
+ await userEvent.click(screen.getByRole('button', { name: /add creative/i }));
1827
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1828
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Mobile push'));
1829
+ await waitFor(() => expect(screen.getByTestId('creatives-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1830
+ expect(screen.getByTestId('creatives-mock')).toHaveAttribute('data-creatives-channel', 'mobilepush');
1831
+ });
1832
+
1833
+ // ── messageMetaConfigId from templateId ──────────────────────────────────────
1834
+
1835
+ it('preview passes messageMetaConfigId from templateConfigs.templateId', async () => {
1836
+ renderStep(
1837
+ <ChannelSelectionStep
1838
+ value={{
1839
+ contentItems: [{
1840
+ contentId: 'sms-tid',
1841
+ channel: 'SMS',
1842
+ templateData: {
1843
+ smsBody: 'Body',
1844
+ templateConfigs: { templateId: 'tmpl-42' },
1845
+ },
1846
+ }],
1847
+ }}
1848
+ onChange={jest.fn()}
1849
+ channels={CHANNELS}
1850
+ />,
1851
+ );
1852
+ await userEvent.click(screen.getByLabelText('Show more content options icon'));
1853
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1854
+ await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
1855
+ await waitFor(() => expect(screen.getByTestId('test-and-preview-mock')).toBeInTheDocument(), WAIT_OPTIONS);
1856
+ });
1857
+
1858
+ // ── handleChannelsVisibleChange / handleIncentivesVisibleChange ───────────────
1859
+
1860
+ it('closing the channel dropdown via onVisibleChange(false) hides the overlay', async () => {
1861
+ renderStep(
1862
+ <ChannelSelectionStep
1863
+ value={{ contentItems: [] }}
1864
+ onChange={jest.fn()}
1865
+ channels={CHANNELS}
1866
+ />,
1867
+ );
1868
+ await userEvent.click(screen.getByRole('button', { name: /add creative/i }));
1869
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
1870
+ // Click outside to close
1871
+ await userEvent.keyboard('{Escape}');
1872
+ // After close the menu should not be visible (or at least no crash)
1873
+ expect(screen.getByRole('button', { name: /add creative/i })).toBeInTheDocument();
1874
+ });
1875
+
1876
+ // ── availableIncentiveTypes null/empty guard ─────────────────────────────────
1877
+
1878
+ it('incentivesData with null types does not crash and shows no incentive button', () => {
1879
+ renderStep(
1880
+ <ChannelSelectionStep
1881
+ value={{ contentItems: [] }}
1882
+ onChange={jest.fn()}
1883
+ channels={CHANNELS}
1884
+ incentivesData={{ enabled: true, types: null }}
1885
+ />,
1886
+ );
1887
+ expect(screen.queryByRole('button', { name: /add incentive/i })).not.toBeInTheDocument();
1888
+ });
1889
+
1890
+ it('incentivesData with empty types array returns null from renderIncentiveDropdownOverlay', () => {
1891
+ // Exercises line 471: availableIncentiveTypes?.length === 0 → return null
1892
+ renderStep(
1893
+ <ChannelSelectionStep
1894
+ value={{ contentItems: [] }}
1895
+ onChange={jest.fn()}
1896
+ channels={CHANNELS}
1897
+ incentivesData={{ enabled: true, types: [] }}
1898
+ />,
1899
+ );
1900
+ expect(screen.queryByRole('button', { name: /add incentive/i })).not.toBeInTheDocument();
1901
+ });
1902
+
1903
+ // ── config.context.ouId passed to TestAndPreview ──────────────────────────────
1904
+
1905
+ it('config prop is accepted without crash', () => {
1906
+ renderStep(
1907
+ <ChannelSelectionStep
1908
+ value={{ contentItems: [] }}
1909
+ onChange={jest.fn()}
1910
+ channels={CHANNELS}
1911
+ config={{ context: { ouId: 'ou-123' } }}
1912
+ />,
1913
+ );
1914
+ expect(screen.getByRole('button', { name: /add creative/i })).toBeInTheDocument();
1915
+ });
804
1916
  });