@capillarytech/creatives-library 8.0.318 → 8.0.319

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 (36) hide show
  1. package/constants/unified.js +1 -0
  2. package/package.json +1 -1
  3. package/services/api.js +6 -0
  4. package/services/tests/api.test.js +7 -0
  5. package/utils/common.js +6 -1
  6. package/v2Containers/CommunicationFlow/CommunicationFlow.js +291 -0
  7. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +25 -0
  8. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +255 -0
  9. package/v2Containers/CommunicationFlow/constants.js +200 -0
  10. package/v2Containers/CommunicationFlow/index.js +102 -0
  11. package/v2Containers/CommunicationFlow/messages.js +346 -0
  12. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +522 -0
  13. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +170 -0
  14. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +796 -0
  15. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/index.js +5 -0
  16. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/CommunicationStrategyStep.js +95 -0
  17. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/Tests/CommunicationStrategyStep.test.js +133 -0
  18. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/index.js +5 -0
  19. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +289 -0
  20. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.scss +70 -0
  21. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +319 -0
  22. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.scss +69 -0
  23. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +616 -0
  24. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +577 -0
  25. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +1111 -0
  26. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +696 -0
  27. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/index.js +7 -0
  28. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.js +102 -0
  29. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.scss +36 -0
  30. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/Tests/DynamicControlsStep.test.js +91 -0
  31. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/index.js +5 -0
  32. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/MessageTypeStep.js +86 -0
  33. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/Tests/MessageTypeStep.test.js +100 -0
  34. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/index.js +5 -0
  35. package/v2Containers/CommunicationFlow/utils/getEnabledSteps.js +30 -0
  36. package/v2Containers/CreativesContainer/constants.js +3 -0
@@ -0,0 +1,616 @@
1
+ /**
2
+ * DeliverySettingsSection: user journeys after the marketer has added channel content.
3
+ * The section loads domainProperties for those channels, shows a summary row, and opens
4
+ * SenderDetails to adjust senders. Viber can fall back to WeCRM when DGM returns empty contactInfo.
5
+ */
6
+ import React from 'react';
7
+ import {
8
+ render, screen, waitFor, within,
9
+ } from '@testing-library/react';
10
+ import userEvent from '@testing-library/user-event';
11
+ import '@testing-library/jest-dom';
12
+ import { IntlProvider } from 'react-intl';
13
+ import DeliverySettingsSection from '../DeliverySettingsSection';
14
+ import { getDomainProperties, fetchWeCrmAccounts } from '../../../../../services/api';
15
+ import { loadItem } from '../../../../../services/localStorageApi';
16
+
17
+ jest.mock('../../../../../services/api', () => ({
18
+ getDomainProperties: jest.fn(),
19
+ fetchWeCrmAccounts: jest.fn(),
20
+ }));
21
+
22
+ jest.mock('../../../../../services/localStorageApi', () => ({
23
+ loadItem: jest.fn(),
24
+ }));
25
+
26
+ const WABA_ID = 'waba-test-1';
27
+
28
+ /** Typical `getDomainProperties` payloads per channel (merged when testing multi-channel). */
29
+ const apiEntity = {
30
+ SMS: {
31
+ SMS: [
32
+ {
33
+ domainProperties: {
34
+ id: 'sms-dom-1',
35
+ domainName: 'SMS gateway',
36
+ contactInfo: [
37
+ {
38
+ type: 'gsm_sender_id', value: '+1002003001', valid: true, default: true,
39
+ },
40
+ {
41
+ type: 'gsm_sender_id', value: '+1002003002', valid: true, default: false,
42
+ },
43
+ ],
44
+ },
45
+ },
46
+ ],
47
+ },
48
+ EMAIL: {
49
+ EMAIL: [
50
+ {
51
+ domainProperties: {
52
+ id: 'em-1',
53
+ domainName: 'Email gateway',
54
+ contactInfo: [
55
+ {
56
+ type: 'sender_id',
57
+ value: 'notify@brand.com',
58
+ label: 'Brand',
59
+ valid: true,
60
+ default: true,
61
+ },
62
+ ],
63
+ },
64
+ },
65
+ ],
66
+ },
67
+ VIBER: {
68
+ VIBER: [
69
+ {
70
+ domainProperties: {
71
+ id: 'vb-1',
72
+ domainName: 'Viber gateway',
73
+ contactInfo: [
74
+ {
75
+ type: 'gsm_sender_id', value: 'viber-sender-99', valid: true, default: true,
76
+ },
77
+ ],
78
+ },
79
+ },
80
+ ],
81
+ },
82
+ ZALO: {
83
+ ZALO: [
84
+ {
85
+ domainProperties: {
86
+ id: 'zl-1',
87
+ domainName: 'Zalo OA',
88
+ contactInfo: [
89
+ {
90
+ type: 'gsm_sender_id', value: 'zalo-sender-77', valid: true, default: true,
91
+ },
92
+ ],
93
+ },
94
+ },
95
+ ],
96
+ },
97
+ LINE: {
98
+ LINE: [
99
+ {
100
+ domainProperties: {
101
+ id: 'ln-1',
102
+ domainName: 'LINE @brand',
103
+ contactInfo: [
104
+ {
105
+ type: 'gsm_sender_id', value: 'line@account-id', valid: true, default: true,
106
+ },
107
+ ],
108
+ },
109
+ },
110
+ ],
111
+ },
112
+ RCS: {
113
+ RCS: [
114
+ {
115
+ domainProperties: {
116
+ id: 'rcs-1',
117
+ domainName: 'RCS brand',
118
+ contactInfo: [
119
+ {
120
+ type: 'gsm_sender_id', value: '+12025550123', valid: true, default: true,
121
+ },
122
+ ],
123
+ },
124
+ },
125
+ ],
126
+ },
127
+ WHATSAPP: {
128
+ WHATSAPP: [
129
+ {
130
+ domainProperties: {
131
+ id: 'wa-dom-1',
132
+ domainName: 'WA business',
133
+ connectionProperties: { sourceAccountIdentifier: WABA_ID },
134
+ contactInfo: [
135
+ {
136
+ type: 'gsm_sender_id', value: '+441234567890', valid: true, default: true,
137
+ },
138
+ ],
139
+ },
140
+ },
141
+ ],
142
+ },
143
+ };
144
+
145
+ function renderSection(props = {}) {
146
+ const {
147
+ contentItems = [{ channel: 'SMS' }],
148
+ deliverySettingsData = {},
149
+ deliverySetting = {},
150
+ onDeliverySettingChange,
151
+ } = props;
152
+ return render(
153
+ <IntlProvider locale="en" messages={{}} defaultLocale="en">
154
+ <DeliverySettingsSection
155
+ contentItems={contentItems}
156
+ deliverySettingsData={deliverySettingsData}
157
+ deliverySetting={deliverySetting}
158
+ onDeliverySettingChange={onDeliverySettingChange}
159
+ />
160
+ </IntlProvider>,
161
+ );
162
+ }
163
+
164
+ async function openSenderDetailsFromSummary() {
165
+ const heading = await screen.findByText('Sender details');
166
+ const row = heading.closest('.delivery-settings-section--clickable');
167
+ await userEvent.click(row);
168
+ await waitFor(() => {
169
+ expect(document.querySelector('.sender-details')).toBeInTheDocument();
170
+ });
171
+ }
172
+
173
+ describe('DeliverySettingsSection — marketer flows', () => {
174
+ beforeEach(() => {
175
+ jest.clearAllMocks();
176
+ loadItem.mockImplementation((key) => (key === 'ouId' || key === 'orgID' ? 'test-ou' : null));
177
+ fetchWeCrmAccounts.mockResolvedValue({ response: [] });
178
+ });
179
+
180
+ describe('When the block should not appear', () => {
181
+ it('hides delivery settings if the feature is not enabled for the flow', () => {
182
+ getDomainProperties.mockResolvedValue({ entity: apiEntity.SMS });
183
+
184
+ renderSection({
185
+ contentItems: [{ channel: 'SMS' }],
186
+ deliverySettingsData: null,
187
+ });
188
+
189
+ expect(screen.queryByText('Sender details')).not.toBeInTheDocument();
190
+ expect(getDomainProperties).not.toHaveBeenCalled();
191
+ });
192
+
193
+ it('hides when there is no content yet (nothing to derive channels from)', () => {
194
+ getDomainProperties.mockResolvedValue({ entity: apiEntity.SMS });
195
+
196
+ renderSection({ contentItems: [], deliverySettingsData: {} });
197
+
198
+ expect(screen.queryByText('Sender details')).not.toBeInTheDocument();
199
+ });
200
+
201
+ it('hides when every configured template is push/in-app (no delivery senders)', () => {
202
+ getDomainProperties.mockResolvedValue({ entity: {} });
203
+
204
+ renderSection({
205
+ contentItems: [{ channel: 'MPUSH' }, { channel: 'INAPP' }],
206
+ deliverySettingsData: {},
207
+ });
208
+
209
+ expect(screen.queryByText('Sender details')).not.toBeInTheDocument();
210
+ expect(getDomainProperties).not.toHaveBeenCalled();
211
+ });
212
+ });
213
+
214
+ describe('After domainProperties loads — summary strip', () => {
215
+ it('shows Sender ID and the default SMS number from the gateway', async () => {
216
+ getDomainProperties.mockResolvedValue({ entity: apiEntity.SMS });
217
+
218
+ renderSection({ contentItems: [{ channel: 'SMS' }] });
219
+
220
+ await waitFor(() => {
221
+ expect(screen.getByText('Sender details')).toBeInTheDocument();
222
+ expect(screen.getByText('Sender ID')).toBeInTheDocument();
223
+ expect(screen.getByText('+1002003001')).toBeInTheDocument();
224
+ });
225
+ });
226
+
227
+ it('shows WhatsApp sender number and uses template WABA + display name from the creative', async () => {
228
+ getDomainProperties.mockResolvedValue({ entity: apiEntity.WHATSAPP });
229
+
230
+ renderSection({
231
+ contentItems: [
232
+ {
233
+ channel: 'WHATSAPP',
234
+ templateData: { sourceAccountIdentifier: WABA_ID, accountName: 'Shop name' },
235
+ },
236
+ ],
237
+ });
238
+
239
+ await waitFor(() => {
240
+ expect(screen.getByText('Sender number')).toBeInTheDocument();
241
+ expect(screen.getByText('+441234567890')).toBeInTheDocument();
242
+ });
243
+ });
244
+
245
+ it('lists one row per delivery channel when the creative is multi-channel', async () => {
246
+ const merged = {
247
+ ...apiEntity.SMS,
248
+ ...apiEntity.EMAIL,
249
+ ...apiEntity.VIBER,
250
+ };
251
+ getDomainProperties.mockResolvedValue({ entity: merged });
252
+
253
+ renderSection({
254
+ contentItems: [{ channel: 'SMS' }, { channel: 'EMAIL' }, { channel: 'VIBER' }],
255
+ });
256
+
257
+ await waitFor(() => {
258
+ expect(screen.getByText('+1002003001')).toBeInTheDocument();
259
+ expect(screen.getByText('notify@brand.com')).toBeInTheDocument();
260
+ expect(screen.getByText('viber-sender-99')).toBeInTheDocument();
261
+ });
262
+ expect(screen.getAllByText('Sender ID').length).toBeGreaterThanOrEqual(3);
263
+ });
264
+
265
+ it('maps ZALO, LINE, and RCS labels the way the summary card expects', async () => {
266
+ getDomainProperties.mockResolvedValue({
267
+ entity: {
268
+ ...apiEntity.ZALO,
269
+ ...apiEntity.LINE,
270
+ ...apiEntity.RCS,
271
+ },
272
+ });
273
+
274
+ renderSection({
275
+ contentItems: [{ channel: 'ZALO' }, { channel: 'LINE' }, { channel: 'RCS' }],
276
+ });
277
+
278
+ await waitFor(() => {
279
+ expect(screen.getByText('zalo-sender-77')).toBeInTheDocument();
280
+ expect(screen.getByText('Account')).toBeInTheDocument();
281
+ expect(screen.getByText('line@account-id')).toBeInTheDocument();
282
+ expect(screen.getByText('+12025550123')).toBeInTheDocument();
283
+ });
284
+ });
285
+ });
286
+
287
+ describe('API behaviour the UI must tolerate', () => {
288
+ it('uses org unit from local storage (ouId, else orgID) when requesting gateways', async () => {
289
+ loadItem.mockImplementation((key) => (key === 'orgID' ? 'fallback-org' : null));
290
+ getDomainProperties.mockResolvedValue({ entity: apiEntity.SMS });
291
+
292
+ renderSection({ contentItems: [{ channel: 'SMS' }] });
293
+
294
+ await waitFor(() => expect(getDomainProperties).toHaveBeenCalledWith(['SMS'], 'fallback-org'));
295
+ });
296
+
297
+ it('accepts the whole response as the entity when `entity` is omitted (some clients)', async () => {
298
+ getDomainProperties.mockResolvedValue(apiEntity.SMS);
299
+
300
+ renderSection({ contentItems: [{ channel: 'SMS' }] });
301
+
302
+ await waitFor(() => {
303
+ expect(screen.getByText('+1002003001')).toBeInTheDocument();
304
+ });
305
+ });
306
+
307
+ it('normalizes mixed-case channel keys from the API before parsing', async () => {
308
+ getDomainProperties.mockResolvedValue({
309
+ entity: {
310
+ sms: apiEntity.SMS.SMS,
311
+ },
312
+ });
313
+
314
+ renderSection({ contentItems: [{ channel: 'SMS' }] });
315
+
316
+ await waitFor(() => {
317
+ expect(screen.getByText('+1002003001')).toBeInTheDocument();
318
+ });
319
+ });
320
+
321
+ it('shows only the heading when domainProperties fails (user still opens settings to fix)', async () => {
322
+ getDomainProperties.mockRejectedValue(new Error('network'));
323
+
324
+ renderSection({ contentItems: [{ channel: 'SMS' }] });
325
+
326
+ await waitFor(() => {
327
+ expect(screen.getByText('Sender details')).toBeInTheDocument();
328
+ });
329
+ expect(screen.queryByText('+1002003001')).not.toBeInTheDocument();
330
+ expect(screen.queryByText('Sender ID')).not.toBeInTheDocument();
331
+ });
332
+
333
+ it('does not refetch domainProperties when the channel set is unchanged (parent re-render)', async () => {
334
+ getDomainProperties.mockResolvedValue({ entity: apiEntity.SMS });
335
+
336
+ const { rerender } = render(
337
+ <IntlProvider locale="en" messages={{}} defaultLocale="en">
338
+ <DeliverySettingsSection
339
+ contentItems={[{ channel: 'SMS', contentId: 'a' }]}
340
+ deliverySettingsData={{}}
341
+ deliverySetting={{}}
342
+ />
343
+ </IntlProvider>,
344
+ );
345
+
346
+ await waitFor(() => expect(getDomainProperties).toHaveBeenCalledTimes(1));
347
+ await waitFor(() => expect(screen.getByText('+1002003001')).toBeInTheDocument());
348
+
349
+ rerender(
350
+ <IntlProvider locale="en" messages={{}} defaultLocale="en">
351
+ <DeliverySettingsSection
352
+ contentItems={[{ channel: 'SMS', contentId: 'b' }]}
353
+ deliverySettingsData={{}}
354
+ deliverySetting={{}}
355
+ />
356
+ </IntlProvider>,
357
+ );
358
+
359
+ await waitFor(() => expect(getDomainProperties).toHaveBeenCalledTimes(1));
360
+ });
361
+ });
362
+
363
+ describe('Viber + WeCRM fallback (empty DGM contactInfo)', () => {
364
+ const viberEmptyContact = {
365
+ VIBER: [
366
+ {
367
+ domainProperties: {
368
+ id: 'vb-empty',
369
+ domainName: 'Viber DGM row',
370
+ contactInfo: [],
371
+ },
372
+ },
373
+ {
374
+ domainProperties: {
375
+ id: 'vb-2',
376
+ domainName: 'Second row',
377
+ contactInfo: [],
378
+ },
379
+ },
380
+ ],
381
+ };
382
+
383
+ it('merges active WeCRM accounts into the first Viber gateway so senders appear in summary', async () => {
384
+ fetchWeCrmAccounts.mockResolvedValue({
385
+ response: [
386
+ {
387
+ id: 10,
388
+ isActive: true,
389
+ sourceAccountIdentifier: 'wecrm-src',
390
+ name: 'WeCRM Viber',
391
+ },
392
+ {
393
+ id: 11,
394
+ isActive: false,
395
+ sourceAccountIdentifier: 'inactive',
396
+ },
397
+ ],
398
+ });
399
+ getDomainProperties.mockResolvedValue({ entity: viberEmptyContact });
400
+
401
+ renderSection({ contentItems: [{ channel: 'VIBER' }] });
402
+
403
+ await waitFor(() => {
404
+ expect(fetchWeCrmAccounts).toHaveBeenCalledWith('VIBER');
405
+ expect(screen.getByText('wecrm-src')).toBeInTheDocument();
406
+ });
407
+ });
408
+
409
+ it('uses configs.viber_account_name as label when the account has no display name', async () => {
410
+ fetchWeCrmAccounts.mockResolvedValue({
411
+ response: [
412
+ {
413
+ id: 20,
414
+ isActive: true,
415
+ sourceAccountIdentifier: 'id-20',
416
+ configs: { viber_account_name: 'Config label' },
417
+ },
418
+ ],
419
+ });
420
+ getDomainProperties.mockResolvedValue({ entity: viberEmptyContact });
421
+
422
+ renderSection({ contentItems: [{ channel: 'VIBER' }] });
423
+
424
+ await waitFor(() => {
425
+ expect(screen.getByText('id-20')).toBeInTheDocument();
426
+ });
427
+ });
428
+
429
+ it('treats a single WeCRM object the same as a one-element list', async () => {
430
+ fetchWeCrmAccounts.mockResolvedValue({
431
+ response: { id: 30, isActive: true, sourceAccountIdentifier: 'solo' },
432
+ });
433
+ getDomainProperties.mockResolvedValue({ entity: viberEmptyContact });
434
+
435
+ renderSection({ contentItems: [{ channel: 'VIBER' }] });
436
+
437
+ await waitFor(() => {
438
+ expect(screen.getByText('solo')).toBeInTheDocument();
439
+ });
440
+ });
441
+
442
+ it('leaves summary empty-line when WeCRM returns nothing usable', async () => {
443
+ fetchWeCrmAccounts.mockResolvedValue({ response: null });
444
+ getDomainProperties.mockResolvedValue({ entity: viberEmptyContact });
445
+
446
+ renderSection({ contentItems: [{ channel: 'VIBER' }] });
447
+
448
+ await waitFor(() => {
449
+ expect(screen.getByText('Sender details')).toBeInTheDocument();
450
+ });
451
+ expect(screen.queryByText('Sender ID')).not.toBeInTheDocument();
452
+ });
453
+
454
+ it('survives WeCRM errors without breaking the section', async () => {
455
+ fetchWeCrmAccounts.mockRejectedValue(new Error('WeCRM down'));
456
+ getDomainProperties.mockResolvedValue({ entity: viberEmptyContact });
457
+
458
+ renderSection({ contentItems: [{ channel: 'VIBER' }] });
459
+
460
+ await waitFor(() => {
461
+ expect(screen.getByText('Sender details')).toBeInTheDocument();
462
+ });
463
+ });
464
+ });
465
+
466
+ describe('Saved delivery choices (edit existing creative)', () => {
467
+ it('prefers persisted channelSetting values over the raw API defaults in the summary', async () => {
468
+ getDomainProperties.mockResolvedValue({ entity: apiEntity.SMS });
469
+
470
+ renderSection({
471
+ contentItems: [{ channel: 'SMS' }],
472
+ deliverySetting: {
473
+ channelSetting: {
474
+ SMS: { gsmSenderId: '+saved-from-campaign', domainId: 'sms-dom-1' },
475
+ },
476
+ },
477
+ });
478
+
479
+ await waitFor(() => {
480
+ expect(screen.getByText('+saved-from-campaign')).toBeInTheDocument();
481
+ });
482
+ });
483
+ });
484
+
485
+ describe('Opening SenderDetails and saving', () => {
486
+ it('shows gateway errors inside the slidebox when SMS gateways are missing or misconfigured', async () => {
487
+ getDomainProperties.mockResolvedValue({ entity: { SMS: [] } });
488
+
489
+ renderSection({ contentItems: [{ channel: 'SMS' }] });
490
+
491
+ await waitFor(() => expect(screen.getByText('Sender details')).toBeInTheDocument());
492
+ await openSenderDetailsFromSummary();
493
+
494
+ const slidebox = document.querySelector('.sender-details');
495
+ await waitFor(() => {
496
+ expect(
497
+ within(slidebox).getByText(/Domain gateway id is not found for the selected channel/i),
498
+ ).toBeInTheDocument();
499
+ });
500
+ });
501
+
502
+ it('shows sender configuration error when the gateway exists but has no senders', async () => {
503
+ getDomainProperties.mockResolvedValue({
504
+ entity: {
505
+ SMS: [
506
+ {
507
+ domainProperties: {
508
+ id: 'sms-no-senders',
509
+ domainName: 'Empty gateway',
510
+ contactInfo: [],
511
+ },
512
+ },
513
+ ],
514
+ },
515
+ });
516
+
517
+ renderSection({ contentItems: [{ channel: 'SMS' }] });
518
+
519
+ await waitFor(() => expect(screen.getByText('Sender details')).toBeInTheDocument());
520
+ await openSenderDetailsFromSummary();
521
+
522
+ const slidebox = document.querySelector('.sender-details');
523
+ await waitFor(() => {
524
+ expect(
525
+ within(slidebox).getByText(/Selected domain gateway id is not correct/i),
526
+ ).toBeInTheDocument();
527
+ });
528
+ });
529
+
530
+ it('persists SMS sender changes back through onDeliverySettingChange', async () => {
531
+ getDomainProperties.mockResolvedValue({ entity: apiEntity.SMS });
532
+ const onDeliverySettingChange = jest.fn();
533
+
534
+ renderSection({
535
+ contentItems: [{ channel: 'SMS' }],
536
+ onDeliverySettingChange,
537
+ });
538
+
539
+ await waitFor(() => expect(screen.getByText('Sender details')).toBeInTheDocument());
540
+ await openSenderDetailsFromSummary();
541
+
542
+ const slidebox = document.querySelector('.sender-details');
543
+ const combos = within(slidebox).getAllByRole('combobox');
544
+ await userEvent.click(combos[1]);
545
+ await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument());
546
+ await userEvent.click(within(screen.getByRole('listbox')).getByText('+1002003002'));
547
+
548
+ const saveBtn = within(slidebox).getByRole('button', { name: /save changes/i });
549
+ await waitFor(() => expect(saveBtn).not.toBeDisabled());
550
+ await userEvent.click(saveBtn);
551
+
552
+ expect(onDeliverySettingChange).toHaveBeenCalled();
553
+ const payload = onDeliverySettingChange.mock.calls[0][0];
554
+ expect(payload.channelSetting.SMS).toMatchObject({
555
+ gsmSenderId: '+1002003002',
556
+ });
557
+ });
558
+
559
+ it('does not throw if the parent omits onDeliverySettingChange and the user saves', async () => {
560
+ getDomainProperties.mockResolvedValue({ entity: apiEntity.SMS });
561
+
562
+ renderSection({
563
+ contentItems: [{ channel: 'SMS' }],
564
+ onDeliverySettingChange: undefined,
565
+ });
566
+
567
+ await waitFor(() => expect(screen.getByText('Sender details')).toBeInTheDocument());
568
+ await openSenderDetailsFromSummary();
569
+
570
+ const slidebox = document.querySelector('.sender-details');
571
+ const combos = within(slidebox).getAllByRole('combobox');
572
+ await userEvent.click(combos[1]);
573
+ await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument());
574
+ await userEvent.click(within(screen.getByRole('listbox')).getByText('+1002003002'));
575
+ const saveBtn = within(slidebox).getByRole('button', { name: /save changes/i });
576
+ await waitFor(() => expect(saveBtn).not.toBeDisabled());
577
+ await userEvent.click(saveBtn);
578
+ });
579
+ });
580
+
581
+ describe('DeduplicationRef — skips re-fetch when data already loaded for the same channels', () => {
582
+ it('does not issue a second network call when the same channels are re-rendered with the same deliverySettingsData', async () => {
583
+ // Covers line 85-86: lastChannelKeyRef === deliveryChannelKey && domainPropertiesData already set
584
+ getDomainProperties.mockResolvedValue({ entity: apiEntity.SMS });
585
+
586
+ const { rerender } = render(
587
+ <IntlProvider locale="en" messages={{}} defaultLocale="en">
588
+ <DeliverySettingsSection
589
+ contentItems={[{ channel: 'SMS', id: 'a' }]}
590
+ deliverySettingsData={{}}
591
+ deliverySetting={{}}
592
+ />
593
+ </IntlProvider>,
594
+ );
595
+
596
+ // Wait for the first fetch to complete and data to load
597
+ await waitFor(() => expect(getDomainProperties).toHaveBeenCalledTimes(1));
598
+ await waitFor(() => expect(screen.getByText('+1002003001')).toBeInTheDocument());
599
+
600
+ // Re-render with the same SMS channel — should hit the dedup branch, not fetch again
601
+ rerender(
602
+ <IntlProvider locale="en" messages={{}} defaultLocale="en">
603
+ <DeliverySettingsSection
604
+ contentItems={[{ channel: 'SMS', id: 'b' }]}
605
+ deliverySettingsData={{}}
606
+ deliverySetting={{}}
607
+ />
608
+ </IntlProvider>,
609
+ );
610
+
611
+ // getDomainProperties should still only have been called once
612
+ await waitFor(() => expect(getDomainProperties).toHaveBeenCalledTimes(1));
613
+ });
614
+ });
615
+
616
+ });