@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.
- package/constants/unified.js +1 -0
- package/package.json +1 -1
- package/services/api.js +6 -0
- package/services/tests/api.test.js +7 -0
- package/utils/common.js +6 -1
- package/v2Containers/CommunicationFlow/CommunicationFlow.js +291 -0
- package/v2Containers/CommunicationFlow/CommunicationFlow.scss +25 -0
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +255 -0
- package/v2Containers/CommunicationFlow/constants.js +200 -0
- package/v2Containers/CommunicationFlow/index.js +102 -0
- package/v2Containers/CommunicationFlow/messages.js +346 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +522 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +170 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +796 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/index.js +5 -0
- package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/CommunicationStrategyStep.js +95 -0
- package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/Tests/CommunicationStrategyStep.test.js +133 -0
- package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/index.js +5 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +289 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.scss +70 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +319 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.scss +69 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +616 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +577 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +1111 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +696 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/index.js +7 -0
- package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.js +102 -0
- package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.scss +36 -0
- package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/Tests/DynamicControlsStep.test.js +91 -0
- package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/index.js +5 -0
- package/v2Containers/CommunicationFlow/steps/MessageTypeStep/MessageTypeStep.js +86 -0
- package/v2Containers/CommunicationFlow/steps/MessageTypeStep/Tests/MessageTypeStep.test.js +100 -0
- package/v2Containers/CommunicationFlow/steps/MessageTypeStep/index.js +5 -0
- package/v2Containers/CommunicationFlow/utils/getEnabledSteps.js +30 -0
- package/v2Containers/CreativesContainer/constants.js +3 -0
package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js
ADDED
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
/* eslint-disable react/prop-types */
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import {
|
|
4
|
+
render, screen, waitFor, within,
|
|
5
|
+
} from '@testing-library/react';
|
|
6
|
+
import userEvent from '@testing-library/user-event';
|
|
7
|
+
import '@testing-library/jest-dom';
|
|
8
|
+
import { IntlProvider } from 'react-intl';
|
|
9
|
+
import ChannelSelectionStep from '../ChannelSelectionStep';
|
|
10
|
+
import { CHANNELS } from '../../../constants';
|
|
11
|
+
|
|
12
|
+
jest.mock('../../../../CreativesContainer', () => function MockCreativesContainer({
|
|
13
|
+
getCreativesData,
|
|
14
|
+
handleCloseCreatives,
|
|
15
|
+
creativesMode,
|
|
16
|
+
channel,
|
|
17
|
+
}) {
|
|
18
|
+
return (
|
|
19
|
+
<div data-testid="creatives-mock" data-creatives-mode={creativesMode} data-creatives-channel={channel}>
|
|
20
|
+
<button
|
|
21
|
+
type="button"
|
|
22
|
+
data-testid="creatives-save"
|
|
23
|
+
onClick={() => getCreativesData({ smsBody: 'Saved body' })}
|
|
24
|
+
>
|
|
25
|
+
Save creative
|
|
26
|
+
</button>
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
data-testid="creatives-save-null"
|
|
30
|
+
onClick={() => getCreativesData(null)}
|
|
31
|
+
>
|
|
32
|
+
Save null
|
|
33
|
+
</button>
|
|
34
|
+
<button type="button" data-testid="creatives-close" onClick={handleCloseCreatives}>
|
|
35
|
+
Close creative
|
|
36
|
+
</button>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
jest.mock('../../DeliverySettingsStep', () => ({
|
|
42
|
+
DeliverySettingsSection: function MockDeliverySettings({ onDeliverySettingChange }) {
|
|
43
|
+
return (
|
|
44
|
+
<div data-testid="delivery-settings-section">
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
data-testid="delivery-apply"
|
|
48
|
+
onClick={() => onDeliverySettingChange({ channelSetting: { SMS: { domainId: 'd1' } } })}
|
|
49
|
+
>
|
|
50
|
+
Apply delivery
|
|
51
|
+
</button>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
},
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
function renderStep(ui) {
|
|
58
|
+
return render(
|
|
59
|
+
<IntlProvider locale="en" messages={{}} defaultLocale="en">
|
|
60
|
+
{ui}
|
|
61
|
+
</IntlProvider>,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe('ChannelSelectionStep', () => {
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
console.log.mockRestore();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('empty state: shows content template heading and add-template entry point', () => {
|
|
75
|
+
const onChange = jest.fn();
|
|
76
|
+
renderStep(
|
|
77
|
+
<ChannelSelectionStep
|
|
78
|
+
value={{ contentItems: [] }}
|
|
79
|
+
onChange={onChange}
|
|
80
|
+
channels={CHANNELS}
|
|
81
|
+
/>,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(screen.getAllByText('Content template').length).toBeGreaterThanOrEqual(1);
|
|
85
|
+
expect(screen.getByText('Add message content and incentive')).toBeInTheDocument();
|
|
86
|
+
expect(screen.getByRole('button', { name: /content template/i })).toBeInTheDocument();
|
|
87
|
+
expect(screen.queryByTestId('creatives-mock')).not.toBeInTheDocument();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('shows validation error when error prop is set', () => {
|
|
91
|
+
renderStep(
|
|
92
|
+
<ChannelSelectionStep
|
|
93
|
+
value={{ contentItems: [] }}
|
|
94
|
+
onChange={jest.fn()}
|
|
95
|
+
channels={CHANNELS}
|
|
96
|
+
error="Channel step error"
|
|
97
|
+
/>,
|
|
98
|
+
);
|
|
99
|
+
expect(screen.getByText('Channel step error')).toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('with content: shows preview text, sender line for SMS, and remove updates parent', async () => {
|
|
103
|
+
const onChange = jest.fn();
|
|
104
|
+
renderStep(
|
|
105
|
+
<ChannelSelectionStep
|
|
106
|
+
value={{
|
|
107
|
+
contentItems: [
|
|
108
|
+
{
|
|
109
|
+
contentId: 'id-1',
|
|
110
|
+
channel: 'SMS',
|
|
111
|
+
templateData: {
|
|
112
|
+
smsBody: 'Preview line for card',
|
|
113
|
+
templateConfigs: { registeredSenderIds: 'SID99' },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
}}
|
|
118
|
+
onChange={onChange}
|
|
119
|
+
channels={CHANNELS}
|
|
120
|
+
/>,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
expect(screen.getByText('Preview line for card')).toBeInTheDocument();
|
|
124
|
+
expect(screen.getByText(/SMS sender ID SID99/)).toBeInTheDocument();
|
|
125
|
+
|
|
126
|
+
await userEvent.click(screen.getByLabelText('Show more content options icon'));
|
|
127
|
+
await waitFor(() => {
|
|
128
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
129
|
+
});
|
|
130
|
+
await userEvent.click(within(screen.getByRole('menu')).getByText('Remove'));
|
|
131
|
+
|
|
132
|
+
expect(onChange).toHaveBeenCalledWith({ contentItems: [] });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('add SMS template: opens creatives, save calls onChange with new content item', async () => {
|
|
136
|
+
const onChange = jest.fn();
|
|
137
|
+
renderStep(
|
|
138
|
+
<ChannelSelectionStep
|
|
139
|
+
value={{ contentItems: [] }}
|
|
140
|
+
onChange={onChange}
|
|
141
|
+
channels={CHANNELS}
|
|
142
|
+
/>,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
await userEvent.click(screen.getByRole('button', { name: /content template/i }));
|
|
146
|
+
await waitFor(() => {
|
|
147
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
148
|
+
});
|
|
149
|
+
await userEvent.click(within(screen.getByRole('menu')).getByText('SMS'));
|
|
150
|
+
|
|
151
|
+
await waitFor(() => {
|
|
152
|
+
expect(screen.getByTestId('creatives-mock')).toBeInTheDocument();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(screen.getByTestId('creatives-mock')).toHaveAttribute('data-creatives-mode', 'create');
|
|
156
|
+
|
|
157
|
+
expect(onChange).toHaveBeenCalledWith(
|
|
158
|
+
expect.objectContaining({ channel: 'SMS', channels: [] }),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
await userEvent.click(screen.getByTestId('creatives-save'));
|
|
162
|
+
|
|
163
|
+
expect(onChange).toHaveBeenCalledWith({
|
|
164
|
+
contentItems: [
|
|
165
|
+
expect.objectContaining({
|
|
166
|
+
channel: 'SMS',
|
|
167
|
+
templateData: { smsBody: 'Saved body' },
|
|
168
|
+
}),
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('closes creatives and clears editing state when handleCloseCreatives runs', async () => {
|
|
174
|
+
const onChange = jest.fn();
|
|
175
|
+
renderStep(
|
|
176
|
+
<ChannelSelectionStep
|
|
177
|
+
value={{ contentItems: [] }}
|
|
178
|
+
onChange={onChange}
|
|
179
|
+
channels={CHANNELS}
|
|
180
|
+
/>,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
await userEvent.click(screen.getByRole('button', { name: /content template/i }));
|
|
184
|
+
await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
|
|
185
|
+
await userEvent.click(within(screen.getByRole('menu')).getByText('SMS'));
|
|
186
|
+
await waitFor(() => expect(screen.getByTestId('creatives-mock')).toBeInTheDocument());
|
|
187
|
+
|
|
188
|
+
await userEvent.click(screen.getByTestId('creatives-close'));
|
|
189
|
+
|
|
190
|
+
expect(screen.queryByTestId('creatives-mock')).not.toBeInTheDocument();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('edit existing content: opens creatives in edit mode and replaces item on save', async () => {
|
|
194
|
+
const onChange = jest.fn();
|
|
195
|
+
renderStep(
|
|
196
|
+
<ChannelSelectionStep
|
|
197
|
+
value={{
|
|
198
|
+
contentItems: [
|
|
199
|
+
{
|
|
200
|
+
contentId: 'edit-me',
|
|
201
|
+
channel: 'SMS',
|
|
202
|
+
templateData: { smsBody: 'Before' },
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
}}
|
|
206
|
+
onChange={onChange}
|
|
207
|
+
channels={CHANNELS}
|
|
208
|
+
/>,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
await userEvent.click(screen.getByLabelText('Show more content options icon'));
|
|
212
|
+
await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
|
|
213
|
+
await userEvent.click(within(screen.getByRole('menu')).getByText('Edit'));
|
|
214
|
+
|
|
215
|
+
await waitFor(() => expect(screen.getByTestId('creatives-mock')).toBeInTheDocument());
|
|
216
|
+
expect(screen.getByTestId('creatives-mock')).toHaveAttribute('data-creatives-mode', 'edit');
|
|
217
|
+
|
|
218
|
+
await userEvent.click(screen.getByTestId('creatives-save'));
|
|
219
|
+
|
|
220
|
+
expect(onChange).toHaveBeenCalledWith({
|
|
221
|
+
contentItems: [
|
|
222
|
+
expect.objectContaining({
|
|
223
|
+
contentId: 'edit-me',
|
|
224
|
+
channel: 'SMS',
|
|
225
|
+
templateData: { smsBody: 'Saved body' },
|
|
226
|
+
}),
|
|
227
|
+
],
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('preview menu logs content id', async () => {
|
|
232
|
+
renderStep(
|
|
233
|
+
<ChannelSelectionStep
|
|
234
|
+
value={{
|
|
235
|
+
contentItems: [
|
|
236
|
+
{ contentId: 'p1', channel: 'SMS', templateData: { smsBody: 'x' } },
|
|
237
|
+
],
|
|
238
|
+
}}
|
|
239
|
+
onChange={jest.fn()}
|
|
240
|
+
channels={CHANNELS}
|
|
241
|
+
/>,
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
await userEvent.click(screen.getByLabelText('Show more content options icon'));
|
|
245
|
+
await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
|
|
246
|
+
await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
|
|
247
|
+
|
|
248
|
+
expect(console.log).toHaveBeenCalledWith('Preview content', 'p1');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('delivery settings section forwards onDeliverySettingChange to onChange', async () => {
|
|
252
|
+
const onChange = jest.fn();
|
|
253
|
+
renderStep(
|
|
254
|
+
<ChannelSelectionStep
|
|
255
|
+
value={{
|
|
256
|
+
contentItems: [{ contentId: 'x', channel: 'SMS', templateData: {} }],
|
|
257
|
+
deliverySetting: {},
|
|
258
|
+
}}
|
|
259
|
+
onChange={onChange}
|
|
260
|
+
channels={CHANNELS}
|
|
261
|
+
deliverySettingsData={{}}
|
|
262
|
+
/>,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
await userEvent.click(screen.getByTestId('delivery-apply'));
|
|
266
|
+
|
|
267
|
+
expect(onChange).toHaveBeenCalledWith({
|
|
268
|
+
deliverySetting: { channelSetting: { SMS: { domainId: 'd1' } } },
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('incentive-only row (no content): selecting an incentive updates selectedOfferDetails', async () => {
|
|
273
|
+
const onChange = jest.fn();
|
|
274
|
+
renderStep(
|
|
275
|
+
<ChannelSelectionStep
|
|
276
|
+
value={{ contentItems: [] }}
|
|
277
|
+
onChange={onChange}
|
|
278
|
+
channels={CHANNELS}
|
|
279
|
+
incentivesData={{
|
|
280
|
+
types: [
|
|
281
|
+
{ value: 'badges', label: 'Badges', isActive: true },
|
|
282
|
+
{ value: 'coupons', label: 'Coupons', isActive: true },
|
|
283
|
+
],
|
|
284
|
+
}}
|
|
285
|
+
/>,
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
await userEvent.click(screen.getByRole('button', { name: /^add incentive$/i }));
|
|
289
|
+
await waitFor(() => {
|
|
290
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
291
|
+
});
|
|
292
|
+
await userEvent.click(within(screen.getByRole('menu')).getByText('Badges'));
|
|
293
|
+
|
|
294
|
+
expect(onChange).toHaveBeenCalledWith({
|
|
295
|
+
selectedOfferDetails: [{ type: 'badges' }],
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('card with content shows add-incentive action when incentives are available', async () => {
|
|
300
|
+
const onChange = jest.fn();
|
|
301
|
+
renderStep(
|
|
302
|
+
<ChannelSelectionStep
|
|
303
|
+
value={{
|
|
304
|
+
contentItems: [
|
|
305
|
+
{ contentId: 'c1', channel: 'SMS', templateData: { smsBody: 'Body' } },
|
|
306
|
+
],
|
|
307
|
+
}}
|
|
308
|
+
onChange={onChange}
|
|
309
|
+
channels={CHANNELS}
|
|
310
|
+
incentivesData={{
|
|
311
|
+
types: [{ value: 'badges', label: 'Badges', isActive: true }],
|
|
312
|
+
}}
|
|
313
|
+
/>,
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
const addIncentiveLinks = screen.getAllByRole('button', { name: /add incentive/i });
|
|
317
|
+
expect(addIncentiveLinks.length).toBeGreaterThanOrEqual(1);
|
|
318
|
+
await userEvent.click(addIncentiveLinks[addIncentiveLinks.length - 1]);
|
|
319
|
+
await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
|
|
320
|
+
await userEvent.click(within(screen.getByRole('menu')).getByText('Badges'));
|
|
321
|
+
|
|
322
|
+
expect(onChange).toHaveBeenCalledWith({
|
|
323
|
+
selectedOfferDetails: [{ type: 'badges' }],
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('renders previews for email subject, message body, push title, and email sender footer', () => {
|
|
328
|
+
renderStep(
|
|
329
|
+
<ChannelSelectionStep
|
|
330
|
+
value={{
|
|
331
|
+
contentItems: [
|
|
332
|
+
{ contentId: 'e1', channel: 'EMAIL', templateData: { emailSubject: 'Subject line' } },
|
|
333
|
+
{ contentId: 'm1', channel: 'SMS', templateData: { messageBody: 'Plain message body text' } },
|
|
334
|
+
{
|
|
335
|
+
contentId: 'mp1',
|
|
336
|
+
channel: 'MOBILEPUSH',
|
|
337
|
+
templateData: { notificationTitle: 'Push title here' },
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
contentId: 'em2',
|
|
341
|
+
channel: 'EMAIL',
|
|
342
|
+
templateData: { emailFrom: 'from@example.com' },
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
}}
|
|
346
|
+
onChange={jest.fn()}
|
|
347
|
+
channels={CHANNELS}
|
|
348
|
+
/>,
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
expect(screen.getByText('Subject line')).toBeInTheDocument();
|
|
352
|
+
expect(screen.getByText(/Plain message body text/)).toBeInTheDocument();
|
|
353
|
+
expect(screen.getByText('Push title here')).toBeInTheDocument();
|
|
354
|
+
expect(screen.getByText('from@example.com')).toBeInTheDocument();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('mobile push preview falls back to notification body when title is absent', () => {
|
|
358
|
+
renderStep(
|
|
359
|
+
<ChannelSelectionStep
|
|
360
|
+
value={{
|
|
361
|
+
contentItems: [
|
|
362
|
+
{
|
|
363
|
+
contentId: 'mp-body',
|
|
364
|
+
channel: 'MOBILEPUSH',
|
|
365
|
+
templateData: { notificationBody: 'Body only preview text' },
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
}}
|
|
369
|
+
onChange={jest.fn()}
|
|
370
|
+
channels={CHANNELS}
|
|
371
|
+
/>,
|
|
372
|
+
);
|
|
373
|
+
expect(screen.getByText(/Body only preview text/)).toBeInTheDocument();
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('renders SMS sender from header and Viber sender line', () => {
|
|
377
|
+
renderStep(
|
|
378
|
+
<ChannelSelectionStep
|
|
379
|
+
value={{
|
|
380
|
+
contentItems: [
|
|
381
|
+
{ contentId: 's1', channel: 'SMS', templateData: { header: 'HDR' } },
|
|
382
|
+
{
|
|
383
|
+
contentId: 'v1',
|
|
384
|
+
channel: 'VIBER',
|
|
385
|
+
templateData: { templateConfigs: { viberSenderId: 'VIB-1' } },
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
}}
|
|
389
|
+
onChange={jest.fn()}
|
|
390
|
+
channels={CHANNELS}
|
|
391
|
+
/>,
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
expect(screen.getByText(/SMS sender ID HDR/)).toBeInTheDocument();
|
|
395
|
+
expect(screen.getByText(/Viber sender ID VIB-1/)).toBeInTheDocument();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('channel dropdown returns null when no active channels in list', async () => {
|
|
399
|
+
const onChange = jest.fn();
|
|
400
|
+
renderStep(
|
|
401
|
+
<ChannelSelectionStep
|
|
402
|
+
value={{ contentItems: [] }}
|
|
403
|
+
onChange={onChange}
|
|
404
|
+
channels={[
|
|
405
|
+
{
|
|
406
|
+
value: 'SMS',
|
|
407
|
+
label: 'SMS',
|
|
408
|
+
iconType: 'sms',
|
|
409
|
+
isActive: false,
|
|
410
|
+
paneKey: 'sms',
|
|
411
|
+
channelProp: 'sms',
|
|
412
|
+
},
|
|
413
|
+
]}
|
|
414
|
+
/>,
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
await userEvent.click(screen.getByRole('button', { name: /content template/i }));
|
|
418
|
+
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('channel dropdown returns null when all active channels are hidden', async () => {
|
|
422
|
+
renderStep(
|
|
423
|
+
<ChannelSelectionStep
|
|
424
|
+
value={{ contentItems: [] }}
|
|
425
|
+
onChange={jest.fn()}
|
|
426
|
+
channels={CHANNELS}
|
|
427
|
+
channelsToHide={CHANNELS.map((c) => c.value)}
|
|
428
|
+
/>,
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
await userEvent.click(screen.getByRole('button', { name: /content template/i }));
|
|
432
|
+
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('incentive dropdown is empty when only coupons and points types exist', async () => {
|
|
436
|
+
renderStep(
|
|
437
|
+
<ChannelSelectionStep
|
|
438
|
+
value={{ contentItems: [] }}
|
|
439
|
+
onChange={jest.fn()}
|
|
440
|
+
channels={CHANNELS}
|
|
441
|
+
incentivesData={{
|
|
442
|
+
types: [
|
|
443
|
+
{ value: 'coupons', label: 'Coupons', isActive: true },
|
|
444
|
+
{ value: 'points', label: 'Points', isActive: true },
|
|
445
|
+
],
|
|
446
|
+
}}
|
|
447
|
+
/>,
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
expect(screen.queryByRole('button', { name: /^add incentive$/i })).not.toBeInTheDocument();
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('treats null value as empty contentItems', () => {
|
|
454
|
+
renderStep(
|
|
455
|
+
<ChannelSelectionStep value={null} onChange={jest.fn()} channels={CHANNELS} />,
|
|
456
|
+
);
|
|
457
|
+
expect(screen.getByText('Add message content and incentive')).toBeInTheDocument();
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('uses CHANNELS fallback when channels prop is null', async () => {
|
|
461
|
+
renderStep(
|
|
462
|
+
<ChannelSelectionStep value={{ contentItems: [] }} onChange={jest.fn()} channels={null} />,
|
|
463
|
+
);
|
|
464
|
+
await userEvent.click(screen.getByRole('button', { name: /content template/i }));
|
|
465
|
+
await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
|
|
466
|
+
expect(within(screen.getByRole('menu')).getByText('SMS')).toBeInTheDocument();
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('notifies parent of custom channel but skips creatives when channel is missing from CHANNELS', async () => {
|
|
470
|
+
const onChange = jest.fn();
|
|
471
|
+
renderStep(
|
|
472
|
+
<ChannelSelectionStep
|
|
473
|
+
value={{ contentItems: [] }}
|
|
474
|
+
onChange={onChange}
|
|
475
|
+
channels={[
|
|
476
|
+
{
|
|
477
|
+
value: 'CUSTOM',
|
|
478
|
+
label: 'Custom',
|
|
479
|
+
iconType: 'sms',
|
|
480
|
+
isActive: true,
|
|
481
|
+
paneKey: 'custom',
|
|
482
|
+
channelProp: 'custom',
|
|
483
|
+
},
|
|
484
|
+
]}
|
|
485
|
+
/>,
|
|
486
|
+
);
|
|
487
|
+
await userEvent.click(screen.getByRole('button', { name: /content template/i }));
|
|
488
|
+
await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
|
|
489
|
+
await userEvent.click(screen.getByText('Custom'));
|
|
490
|
+
expect(onChange).toHaveBeenCalledWith({ channel: 'CUSTOM', channels: [] });
|
|
491
|
+
expect(screen.queryByTestId('creatives-mock')).not.toBeInTheDocument();
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('handleCreativesData returns early when save payload is null', async () => {
|
|
495
|
+
const onChange = jest.fn();
|
|
496
|
+
renderStep(
|
|
497
|
+
<ChannelSelectionStep value={{ contentItems: [] }} onChange={onChange} channels={CHANNELS} />,
|
|
498
|
+
);
|
|
499
|
+
await userEvent.click(screen.getByRole('button', { name: /content template/i }));
|
|
500
|
+
await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
|
|
501
|
+
await userEvent.click(within(screen.getByRole('menu')).getByText('SMS'));
|
|
502
|
+
await waitFor(() => expect(screen.getByTestId('creatives-mock')).toBeInTheDocument());
|
|
503
|
+
|
|
504
|
+
await userEvent.click(screen.getByTestId('creatives-save-null'));
|
|
505
|
+
|
|
506
|
+
const callsWithContentItems = onChange.mock.calls.filter(([arg]) => arg && 'contentItems' in arg);
|
|
507
|
+
expect(callsWithContentItems).toHaveLength(0);
|
|
508
|
+
expect(screen.getByTestId('creatives-mock')).toBeInTheDocument();
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it('edit is no-op when content channel is not in CHANNELS config', async () => {
|
|
512
|
+
renderStep(
|
|
513
|
+
<ChannelSelectionStep
|
|
514
|
+
value={{
|
|
515
|
+
contentItems: [
|
|
516
|
+
{ contentId: 'orphan', channel: 'NOT_A_REAL_CHANNEL', templateData: { smsBody: 'x' } },
|
|
517
|
+
],
|
|
518
|
+
}}
|
|
519
|
+
onChange={jest.fn()}
|
|
520
|
+
channels={CHANNELS}
|
|
521
|
+
/>,
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
await userEvent.click(screen.getByLabelText('Show more content options icon'));
|
|
525
|
+
await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
|
|
526
|
+
await userEvent.click(within(screen.getByRole('menu')).getByText('Edit'));
|
|
527
|
+
|
|
528
|
+
expect(screen.queryByTestId('creatives-mock')).not.toBeInTheDocument();
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('passes creativesMode preview when not editing', async () => {
|
|
532
|
+
renderStep(
|
|
533
|
+
<ChannelSelectionStep
|
|
534
|
+
value={{ contentItems: [] }}
|
|
535
|
+
onChange={jest.fn()}
|
|
536
|
+
channels={CHANNELS}
|
|
537
|
+
creativesMode="preview"
|
|
538
|
+
/>,
|
|
539
|
+
);
|
|
540
|
+
await userEvent.click(screen.getByRole('button', { name: /content template/i }));
|
|
541
|
+
await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
|
|
542
|
+
await userEvent.click(within(screen.getByRole('menu')).getByText('SMS'));
|
|
543
|
+
await waitFor(() => expect(screen.getByTestId('creatives-mock')).toBeInTheDocument());
|
|
544
|
+
expect(screen.getByTestId('creatives-mock')).toHaveAttribute('data-creatives-mode', 'preview');
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('appends to selectedOfferDetails when an incentive is already selected', async () => {
|
|
548
|
+
const onChange = jest.fn();
|
|
549
|
+
renderStep(
|
|
550
|
+
<ChannelSelectionStep
|
|
551
|
+
value={{ contentItems: [] }}
|
|
552
|
+
onChange={onChange}
|
|
553
|
+
channels={CHANNELS}
|
|
554
|
+
selectedOfferDetails={[{ type: 'existing' }]}
|
|
555
|
+
incentivesData={{
|
|
556
|
+
types: [{ value: 'badges', label: 'Badges', isActive: true }],
|
|
557
|
+
}}
|
|
558
|
+
/>,
|
|
559
|
+
);
|
|
560
|
+
await userEvent.click(screen.getByRole('button', { name: /^add incentive$/i }));
|
|
561
|
+
await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
|
|
562
|
+
await userEvent.click(within(screen.getByRole('menu')).getByText('Badges'));
|
|
563
|
+
expect(onChange).toHaveBeenCalledWith({
|
|
564
|
+
selectedOfferDetails: [{ type: 'existing' }, { type: 'badges' }],
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('uses template message field and non-string smsBody fallback in preview', () => {
|
|
569
|
+
renderStep(
|
|
570
|
+
<ChannelSelectionStep
|
|
571
|
+
value={{
|
|
572
|
+
contentItems: [
|
|
573
|
+
{ contentId: 'line1', channel: 'LINE', templateData: { message: 'Line message preview' } },
|
|
574
|
+
{ contentId: 'sms-num', channel: 'SMS', templateData: { smsBody: 42 } },
|
|
575
|
+
],
|
|
576
|
+
}}
|
|
577
|
+
onChange={jest.fn()}
|
|
578
|
+
channels={CHANNELS}
|
|
579
|
+
/>,
|
|
580
|
+
);
|
|
581
|
+
expect(screen.getByText('Line message preview')).toBeInTheDocument();
|
|
582
|
+
expect(screen.getByText('42')).toBeInTheDocument();
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('maps MPUSH channel to card type and reads push fields from android/ios payloads', () => {
|
|
586
|
+
renderStep(
|
|
587
|
+
<ChannelSelectionStep
|
|
588
|
+
value={{
|
|
589
|
+
contentItems: [
|
|
590
|
+
{
|
|
591
|
+
contentId: 'mpush1',
|
|
592
|
+
channel: 'MPUSH',
|
|
593
|
+
templateData: { notificationTitle: 'Direct MPUSH title' },
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
contentId: 'and',
|
|
597
|
+
channel: 'MOBILEPUSH',
|
|
598
|
+
templateData: { androidContent: { notificationTitle: 'Android title' } },
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
contentId: 'ios',
|
|
602
|
+
channel: 'MOBILEPUSH',
|
|
603
|
+
templateData: { iosContent: { body: 'Ios body text' } },
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
contentId: 'and-body',
|
|
607
|
+
channel: 'MOBILEPUSH',
|
|
608
|
+
templateData: { androidContent: { notificationBody: 'Android notif body' } },
|
|
609
|
+
},
|
|
610
|
+
],
|
|
611
|
+
}}
|
|
612
|
+
onChange={jest.fn()}
|
|
613
|
+
channels={CHANNELS}
|
|
614
|
+
/>,
|
|
615
|
+
);
|
|
616
|
+
expect(screen.getByText('Direct MPUSH title')).toBeInTheDocument();
|
|
617
|
+
expect(screen.getByText('Android title')).toBeInTheDocument();
|
|
618
|
+
expect(screen.getByText(/Ios body text/)).toBeInTheDocument();
|
|
619
|
+
expect(screen.getByText('Android notif body')).toBeInTheDocument();
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it('Viber sender falls back to root viberSenderId', () => {
|
|
623
|
+
renderStep(
|
|
624
|
+
<ChannelSelectionStep
|
|
625
|
+
value={{
|
|
626
|
+
contentItems: [
|
|
627
|
+
{
|
|
628
|
+
contentId: 'v2',
|
|
629
|
+
channel: 'VIBER',
|
|
630
|
+
templateData: { viberSenderId: 'ROOT-V' },
|
|
631
|
+
},
|
|
632
|
+
],
|
|
633
|
+
}}
|
|
634
|
+
onChange={jest.fn()}
|
|
635
|
+
channels={CHANNELS}
|
|
636
|
+
/>,
|
|
637
|
+
);
|
|
638
|
+
expect(screen.getByText(/Viber sender ID ROOT-V/)).toBeInTheDocument();
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it('SMS card without sender id omits sender row', () => {
|
|
642
|
+
renderStep(
|
|
643
|
+
<ChannelSelectionStep
|
|
644
|
+
value={{
|
|
645
|
+
contentItems: [
|
|
646
|
+
{ contentId: 'sms-plain', channel: 'SMS', templateData: { smsBody: 'Only body' } },
|
|
647
|
+
],
|
|
648
|
+
}}
|
|
649
|
+
onChange={jest.fn()}
|
|
650
|
+
channels={CHANNELS}
|
|
651
|
+
/>,
|
|
652
|
+
);
|
|
653
|
+
expect(screen.getByText('Only body')).toBeInTheDocument();
|
|
654
|
+
expect(screen.queryByText(/SMS sender ID/)).not.toBeInTheDocument();
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it('Viber preview uses catch when JSON.parse throws for {} fallback', () => {
|
|
658
|
+
const realParse = JSON.parse;
|
|
659
|
+
const parseSpy = jest.spyOn(JSON, 'parse').mockImplementation((text, reviver) => {
|
|
660
|
+
if (text === '{}') {
|
|
661
|
+
throw new SyntaxError('forced');
|
|
662
|
+
}
|
|
663
|
+
return realParse.call(JSON, text, reviver);
|
|
664
|
+
});
|
|
665
|
+
renderStep(
|
|
666
|
+
<ChannelSelectionStep
|
|
667
|
+
value={{
|
|
668
|
+
contentItems: [
|
|
669
|
+
{
|
|
670
|
+
contentId: 'vib-parse',
|
|
671
|
+
channel: 'VIBER',
|
|
672
|
+
templateData: { messageBody: '' },
|
|
673
|
+
},
|
|
674
|
+
],
|
|
675
|
+
}}
|
|
676
|
+
onChange={jest.fn()}
|
|
677
|
+
channels={CHANNELS}
|
|
678
|
+
/>,
|
|
679
|
+
);
|
|
680
|
+
expect(screen.getByText('VIBER')).toBeInTheDocument();
|
|
681
|
+
parseSpy.mockRestore();
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it('Viber preview falls back to channel when parsed JSON has no text (empty messageBody)', () => {
|
|
685
|
+
renderStep(
|
|
686
|
+
<ChannelSelectionStep
|
|
687
|
+
value={{
|
|
688
|
+
contentItems: [
|
|
689
|
+
{
|
|
690
|
+
contentId: 'vib-empty',
|
|
691
|
+
channel: 'VIBER',
|
|
692
|
+
templateData: { messageBody: '' },
|
|
693
|
+
},
|
|
694
|
+
],
|
|
695
|
+
}}
|
|
696
|
+
onChange={jest.fn()}
|
|
697
|
+
channels={CHANNELS}
|
|
698
|
+
/>,
|
|
699
|
+
);
|
|
700
|
+
expect(screen.getByText('VIBER')).toBeInTheDocument();
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it('does not open creatives when clicked channel is in channelsToDisable', async () => {
|
|
704
|
+
// Covers line 174-175: channelsToDisable.includes(channelValue) branch
|
|
705
|
+
const onChange = jest.fn();
|
|
706
|
+
renderStep(
|
|
707
|
+
<ChannelSelectionStep
|
|
708
|
+
value={{ contentItems: [] }}
|
|
709
|
+
onChange={onChange}
|
|
710
|
+
channels={CHANNELS}
|
|
711
|
+
channelsToDisable={['SMS']}
|
|
712
|
+
/>,
|
|
713
|
+
);
|
|
714
|
+
await userEvent.click(screen.getByRole('button', { name: /content template/i }));
|
|
715
|
+
await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
|
|
716
|
+
// SMS is in channelsToDisable — clicking it should not call onChange or show creatives
|
|
717
|
+
await userEvent.click(within(screen.getByRole('menu')).getByText('SMS'));
|
|
718
|
+
expect(screen.queryByTestId('creatives-mock')).not.toBeInTheDocument();
|
|
719
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
it('incentive dropdown returns null when all active incentive types are coupons/points only (empty after filter)', async () => {
|
|
723
|
+
// Covers line 277-278: availableIncentiveTypes.length === 0 (all inactive) path
|
|
724
|
+
renderStep(
|
|
725
|
+
<ChannelSelectionStep
|
|
726
|
+
value={{ contentItems: [] }}
|
|
727
|
+
onChange={jest.fn()}
|
|
728
|
+
channels={CHANNELS}
|
|
729
|
+
incentivesData={{
|
|
730
|
+
types: [
|
|
731
|
+
{ value: 'rewards', label: 'Rewards', isActive: false },
|
|
732
|
+
{ value: 'other', label: 'Other', isActive: false },
|
|
733
|
+
],
|
|
734
|
+
}}
|
|
735
|
+
/>,
|
|
736
|
+
);
|
|
737
|
+
// No incentive button should appear since all types are inactive
|
|
738
|
+
expect(screen.queryByRole('button', { name: /^add incentive$/i })).not.toBeInTheDocument();
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it('channel in channelsToDisable still shows in dropdown but selecting is a no-op', async () => {
|
|
742
|
+
// Covers line 180-181: selectedChannelObj.isActive path via disabled channel click
|
|
743
|
+
const onChange = jest.fn();
|
|
744
|
+
renderStep(
|
|
745
|
+
<ChannelSelectionStep
|
|
746
|
+
value={{ contentItems: [] }}
|
|
747
|
+
onChange={onChange}
|
|
748
|
+
channels={[
|
|
749
|
+
{
|
|
750
|
+
value: 'EMAIL',
|
|
751
|
+
label: 'Email',
|
|
752
|
+
iconType: 'email',
|
|
753
|
+
isActive: true,
|
|
754
|
+
paneKey: 'email',
|
|
755
|
+
channelProp: 'email',
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
value: 'SMS',
|
|
759
|
+
label: 'SMS',
|
|
760
|
+
iconType: 'sms',
|
|
761
|
+
isActive: true,
|
|
762
|
+
paneKey: 'sms',
|
|
763
|
+
channelProp: 'sms',
|
|
764
|
+
},
|
|
765
|
+
]}
|
|
766
|
+
channelsToDisable={['EMAIL']}
|
|
767
|
+
/>,
|
|
768
|
+
);
|
|
769
|
+
await userEvent.click(screen.getByRole('button', { name: /content template/i }));
|
|
770
|
+
await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
|
|
771
|
+
// Click the disabled EMAIL menu item — handleChannelSelect should return early
|
|
772
|
+
await userEvent.click(within(screen.getByRole('menu')).getByText('Email'));
|
|
773
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
774
|
+
expect(screen.queryByTestId('creatives-mock')).not.toBeInTheDocument();
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
it('ios notification body shows as preview when iosContent.notificationBody is present', () => {
|
|
778
|
+
// Covers line 125: templateData.iosContent.notificationBody fallback branch
|
|
779
|
+
renderStep(
|
|
780
|
+
<ChannelSelectionStep
|
|
781
|
+
value={{
|
|
782
|
+
contentItems: [
|
|
783
|
+
{
|
|
784
|
+
contentId: 'ios-nb',
|
|
785
|
+
channel: 'MOBILEPUSH',
|
|
786
|
+
templateData: { iosContent: { notificationBody: 'IOS notif body text' } },
|
|
787
|
+
},
|
|
788
|
+
],
|
|
789
|
+
}}
|
|
790
|
+
onChange={jest.fn()}
|
|
791
|
+
channels={CHANNELS}
|
|
792
|
+
/>,
|
|
793
|
+
);
|
|
794
|
+
expect(screen.getByText(/IOS notif body text/)).toBeInTheDocument();
|
|
795
|
+
});
|
|
796
|
+
});
|