@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.
- package/constants/unified.js +0 -3
- package/package.json +1 -1
- package/services/api.js +10 -0
- package/services/tests/api.test.js +83 -0
- package/utils/common.js +0 -8
- package/v2Components/CommonTestAndPreview/UnifiedPreview/WhatsAppPreviewContent.js +5 -3
- package/v2Components/CommonTestAndPreview/index.js +7 -0
- package/v2Components/FormBuilder/_formBuilder.scss +0 -5
- package/v2Components/FormBuilder/index.js +4479 -41
- package/v2Components/NavigationBar/index.js +27 -0
- package/v2Components/NavigationBar/messages.js +4 -0
- package/v2Components/NavigationBar/tests/index.test.js +19 -0
- package/v2Components/NewCallTask/index.js +6 -1
- package/v2Components/TemplatePreview/index.js +4 -2
- package/v2Containers/Cap/index.js +3 -1
- package/v2Containers/CommunicationFlow/CommunicationFlow.js +130 -20
- package/v2Containers/CommunicationFlow/CommunicationFlow.scss +154 -0
- package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +240 -0
- package/v2Containers/CommunicationFlow/DemoPage.js +47 -0
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +369 -2
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +619 -0
- package/v2Containers/CommunicationFlow/Tests/DemoPage.test.js +77 -0
- package/v2Containers/CommunicationFlow/Tests/getContentBody.test.js +933 -0
- package/v2Containers/CommunicationFlow/constants.js +45 -10
- package/v2Containers/CommunicationFlow/index.js +5 -2
- package/v2Containers/CommunicationFlow/messages.js +20 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +94 -31
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +14 -11
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +1144 -32
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/extractContentForPreview.js +183 -0
- package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/CommunicationStrategyStep.js +3 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +39 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +6 -2
- package/v2Containers/CommunicationFlow/utils/getContentBody.js +369 -0
- package/v2Containers/CommunicationFlow/utils/getContentBody.scss +19 -0
- package/v2Containers/CommunicationFlow/utils/getEnabledSteps.js +1 -1
- package/v2Containers/CreativesContainer/constants.js +6 -0
- package/v2Containers/CreativesContainer/index.js +68 -1
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +2 -2
- package/v2Containers/Templates/index.js +2 -2
- package/v2Containers/TemplatesV2/index.js +9 -1
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +41 -34
- package/v2Components/FormBuilder/Classic.js +0 -4487
- package/v2Components/FormBuilder/Functional/FormBuilderShell.js +0 -371
- package/v2Components/FormBuilder/Functional/channels/registry.js +0 -17
- package/v2Components/FormBuilder/Functional/channels/sms/buildSubmitPayload.js +0 -9
- package/v2Components/FormBuilder/Functional/channels/sms/config.js +0 -30
- package/v2Components/FormBuilder/Functional/channels/sms/getEditorErrorDescriptor.js +0 -46
- package/v2Components/FormBuilder/Functional/channels/sms/getLiquidContent.js +0 -13
- package/v2Components/FormBuilder/Functional/channels/sms/index.js +0 -22
- package/v2Components/FormBuilder/Functional/channels/sms/tests/getEditorErrorDescriptor.test.js +0 -52
- package/v2Components/FormBuilder/Functional/channels/sms/tests/getLiquidContent.test.js +0 -25
- package/v2Components/FormBuilder/Functional/channels/sms/tests/validate.test.js +0 -87
- package/v2Components/FormBuilder/Functional/channels/sms/validate.js +0 -89
- package/v2Components/FormBuilder/Functional/constants.js +0 -42
- package/v2Components/FormBuilder/Functional/core/schema/fieldRegistry.js +0 -38
- package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +0 -85
- package/v2Components/FormBuilder/Functional/core/store/formReducer.js +0 -81
- package/v2Components/FormBuilder/Functional/core/store/selectors.js +0 -30
- package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +0 -91
- package/v2Components/FormBuilder/Functional/index.js +0 -26
- package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +0 -59
- package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +0 -31
- package/v2Components/FormBuilder/Functional/layout/Section.js +0 -116
- package/v2Components/FormBuilder/Functional/renderers/smsRenderers.js +0 -258
- package/v2Components/FormBuilder/Functional/tests/channelRegistry.test.js +0 -21
- package/v2Components/FormBuilder/Functional/tests/fieldRegistry.test.js +0 -65
- package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +0 -97
- package/v2Components/FormBuilder/Functional/tests/fixtures/smsParityCases.js +0 -192
- package/v2Components/FormBuilder/Functional/tests/formReducer.test.js +0 -129
- package/v2Components/FormBuilder/Functional/tests/initializeFormState.test.js +0 -132
- package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +0 -40
- package/v2Components/FormBuilder/Functional/tests/section.test.js +0 -99
- package/v2Components/FormBuilder/Functional/tests/selectors.test.js +0 -67
- package/v2Components/FormBuilder/Functional/tests/sms.crossFlowParity.test.js +0 -155
- package/v2Components/FormBuilder/Functional/tests/sms.liquid.test.js +0 -172
- package/v2Components/FormBuilder/Functional/tests/sms.rollout.test.js +0 -122
- package/v2Components/FormBuilder/Functional/tests/sms.shell.parity.test.js +0 -329
- package/v2Components/FormBuilder/Functional/tests/smsRenderers.test.js +0 -160
- package/v2Components/FormBuilder/Functional/tests/toLegacyFormData.test.js +0 -95
- package/v2Components/FormBuilder/tests/__snapshots__/sms.characterization.test.js.snap +0 -114
- package/v2Components/FormBuilder/tests/entryGate.test.js +0 -106
- package/v2Components/FormBuilder/tests/sms.characterization.test.js +0 -336
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
|
|
5
|
+
jest.mock('@capillarytech/cap-ui-library/CapImage', () =>
|
|
6
|
+
function MockCapImage({ src, className, alt }) {
|
|
7
|
+
return <img data-testid="cap-image" src={src} className={className} alt={alt || ''} />;
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
jest.mock('@capillarytech/cap-ui-library/CapLabel', () =>
|
|
11
|
+
function MockCapLabel({ children, type, className }) {
|
|
12
|
+
return <span data-testid="cap-label" data-type={type} className={className}>{children}</span>;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
jest.mock('@capillarytech/cap-ui-library/CapIcon', () =>
|
|
16
|
+
function MockCapIcon({ type }) {
|
|
17
|
+
return <span data-testid={`cap-icon-${type}`} />;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
jest.mock('@capillarytech/cap-ui-library/CapRow', () =>
|
|
21
|
+
function MockCapRow({ children, className, style }) {
|
|
22
|
+
return <div data-testid="cap-row" className={className} style={style}>{children}</div>;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
jest.mock('../../Whatsapp/utils', () => ({
|
|
26
|
+
getWhatsappDocPreview: jest.fn(() => <span data-testid="doc-preview">DocPreview</span>),
|
|
27
|
+
getWhatsappQuickReply: jest.fn(() => <span data-testid="quick-reply">QuickReply</span>),
|
|
28
|
+
getWhatsappCarouselButtonView: jest.fn(() => <span data-testid="carousel-buttons">CarouselButtons</span>),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
jest.mock('../../../assets/videoPlay.svg', () => 'videoPlay.svg');
|
|
32
|
+
|
|
33
|
+
import getContentBody from '../utils/getContentBody';
|
|
34
|
+
import { getWhatsappDocPreview, getWhatsappQuickReply, getWhatsappCarouselButtonView } from '../../Whatsapp/utils';
|
|
35
|
+
|
|
36
|
+
const renderResult = (item) => {
|
|
37
|
+
const result = getContentBody(item);
|
|
38
|
+
const { container } = render(<>{result}</>);
|
|
39
|
+
return container;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
describe('getContentBody', () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
jest.clearAllMocks();
|
|
45
|
+
getWhatsappQuickReply.mockReturnValue(<span data-testid="quick-reply">QuickReply</span>);
|
|
46
|
+
getWhatsappCarouselButtonView.mockReturnValue(<span data-testid="carousel-buttons">CarouselButtons</span>);
|
|
47
|
+
getWhatsappDocPreview.mockReturnValue(<span data-testid="doc-preview">DocPreview</span>);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// SMS
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
describe('SMS', () => {
|
|
54
|
+
it('renders messageBody', () => {
|
|
55
|
+
const { getByTestId } = render(<>{getContentBody({ channel: 'SMS', templateData: { messageBody: 'Hello SMS' } })}</>);
|
|
56
|
+
expect(getByTestId('cap-label')).toHaveTextContent('Hello SMS');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('falls back to smsBody when messageBody is absent', () => {
|
|
60
|
+
const { getByTestId } = render(<>{getContentBody({ channel: 'SMS', templateData: { smsBody: 'Fallback' } })}</>);
|
|
61
|
+
expect(getByTestId('cap-label')).toHaveTextContent('Fallback');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('renders empty string when neither messageBody nor smsBody is present', () => {
|
|
65
|
+
const { getByTestId } = render(<>{getContentBody({ channel: 'SMS', templateData: {} })}</>);
|
|
66
|
+
expect(getByTestId('cap-label')).toHaveTextContent('');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('is case-insensitive — lowercased channel works', () => {
|
|
70
|
+
// lowercase 'sms' goes to default branch, not SMS case — document that behavior
|
|
71
|
+
const { getByTestId } = render(<>{getContentBody({ channel: 'sms', templateData: { messageBody: 'hi' } })}</>);
|
|
72
|
+
expect(getByTestId('cap-label')).toBeInTheDocument();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// EMAIL
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
describe('EMAIL', () => {
|
|
80
|
+
it('renders emailSubject', () => {
|
|
81
|
+
const { getByTestId } = render(<>{getContentBody({ channel: 'EMAIL', templateData: { emailSubject: 'My Subject' } })}</>);
|
|
82
|
+
expect(getByTestId('cap-label')).toHaveTextContent('My Subject');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('renders empty string when emailSubject is absent', () => {
|
|
86
|
+
const { getByTestId } = render(<>{getContentBody({ channel: 'EMAIL', templateData: {} })}</>);
|
|
87
|
+
expect(getByTestId('cap-label')).toHaveTextContent('');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// MOBILE_PUSH (MOBILEPUSH)
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
describe('MOBILE_PUSH', () => {
|
|
95
|
+
const channel = 'MOBILEPUSH';
|
|
96
|
+
|
|
97
|
+
it('renders title and message from androidContent', () => {
|
|
98
|
+
const container = renderResult({
|
|
99
|
+
channel,
|
|
100
|
+
templateData: {
|
|
101
|
+
androidContent: { title: 'Push Title', message: 'Push Msg' },
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
expect(container.querySelector('.mobilepush-container')).toBeInTheDocument();
|
|
105
|
+
expect(screen.getByText('Push Title')).toBeInTheDocument();
|
|
106
|
+
expect(screen.getByText('Push Msg')).toBeInTheDocument();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('falls back to iosContent when androidContent is absent', () => {
|
|
110
|
+
renderResult({
|
|
111
|
+
channel,
|
|
112
|
+
templateData: {
|
|
113
|
+
iosContent: { title: 'iOS Title', message: 'iOS Msg' },
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
expect(screen.getByText('iOS Title')).toBeInTheDocument();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('uses empty object when both androidContent and iosContent are absent', () => {
|
|
120
|
+
renderResult({ channel, templateData: {} });
|
|
121
|
+
// Should render without crash; title/message are empty strings
|
|
122
|
+
expect(document.querySelector('.mobilepush-container')).toBeInTheDocument();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('renders image when image is set and media/carouselData are absent', () => {
|
|
126
|
+
renderResult({
|
|
127
|
+
channel,
|
|
128
|
+
templateData: {
|
|
129
|
+
androidContent: {
|
|
130
|
+
title: 'T',
|
|
131
|
+
message: 'M',
|
|
132
|
+
expandableDetails: { image: 'http://img.png' },
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
const img = document.querySelector('.mobilepush-image');
|
|
137
|
+
expect(img).toBeInTheDocument();
|
|
138
|
+
expect(img).toHaveAttribute('src', 'http://img.png');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('does NOT render image when media.url is present', () => {
|
|
142
|
+
renderResult({
|
|
143
|
+
channel,
|
|
144
|
+
templateData: {
|
|
145
|
+
androidContent: {
|
|
146
|
+
expandableDetails: {
|
|
147
|
+
image: 'http://img.png',
|
|
148
|
+
media: [{ url: 'http://video.mp4' }],
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
expect(document.querySelector('.mobilepush-image.mobilepush-image-padding')).not.toBeInTheDocument();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('does NOT render image when carouselData is non-empty', () => {
|
|
157
|
+
renderResult({
|
|
158
|
+
channel,
|
|
159
|
+
templateData: {
|
|
160
|
+
androidContent: {
|
|
161
|
+
expandableDetails: {
|
|
162
|
+
image: 'http://img.png',
|
|
163
|
+
carouselData: [{ imageUrl: 'http://c.png' }],
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
expect(document.querySelector('.mobilepush-image.mobilepush-image-padding')).not.toBeInTheDocument();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('renders video preview when media[0].url is set', () => {
|
|
172
|
+
renderResult({
|
|
173
|
+
channel,
|
|
174
|
+
templateData: {
|
|
175
|
+
androidContent: {
|
|
176
|
+
expandableDetails: { media: [{ url: 'http://vid.mp4' }] },
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
expect(document.querySelector('.video-preview')).toBeInTheDocument();
|
|
181
|
+
const source = document.querySelector('source');
|
|
182
|
+
expect(source).toHaveAttribute('src', 'http://vid.mp4');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('does not render video preview when media[0] has no url', () => {
|
|
186
|
+
renderResult({
|
|
187
|
+
channel,
|
|
188
|
+
templateData: {
|
|
189
|
+
androidContent: {
|
|
190
|
+
expandableDetails: { media: [{}] },
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
expect(document.querySelector('.video-preview')).not.toBeInTheDocument();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('renders carousel cards when carouselData is non-empty', () => {
|
|
198
|
+
renderResult({
|
|
199
|
+
channel,
|
|
200
|
+
templateData: {
|
|
201
|
+
androidContent: {
|
|
202
|
+
expandableDetails: {
|
|
203
|
+
carouselData: [
|
|
204
|
+
{ imageUrl: 'http://c1.png' },
|
|
205
|
+
{ imageUrl: 'http://c2.png' },
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
expect(document.querySelector('.scroll-container')).toBeInTheDocument();
|
|
212
|
+
expect(document.querySelectorAll('.whatsapp-carousel-container')).toHaveLength(2);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('renders carousel card image when imageUrl is present', () => {
|
|
216
|
+
renderResult({
|
|
217
|
+
channel,
|
|
218
|
+
templateData: {
|
|
219
|
+
androidContent: {
|
|
220
|
+
expandableDetails: {
|
|
221
|
+
carouselData: [{ imageUrl: 'http://card.png' }],
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
const img = document.querySelector('.whatsapp-image');
|
|
227
|
+
expect(img).toHaveAttribute('src', 'http://card.png');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('does not render carousel card image when imageUrl is absent', () => {
|
|
231
|
+
renderResult({
|
|
232
|
+
channel,
|
|
233
|
+
templateData: {
|
|
234
|
+
androidContent: {
|
|
235
|
+
expandableDetails: { carouselData: [{}] },
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
expect(document.querySelector('.whatsapp-image')).not.toBeInTheDocument();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('renders CTAs when ctas is non-empty', () => {
|
|
243
|
+
renderResult({
|
|
244
|
+
channel,
|
|
245
|
+
templateData: {
|
|
246
|
+
androidContent: {
|
|
247
|
+
expandableDetails: {
|
|
248
|
+
ctas: [{ actionText: 'Click Me' }, { actionText: 'Open' }],
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
expect(document.querySelector('.actions')).toBeInTheDocument();
|
|
254
|
+
expect(screen.getByText('CLICK ME')).toBeInTheDocument();
|
|
255
|
+
expect(screen.getByText('OPEN')).toBeInTheDocument();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('does not render CTAs section when ctas is empty', () => {
|
|
259
|
+
renderResult({
|
|
260
|
+
channel,
|
|
261
|
+
templateData: { androidContent: { expandableDetails: { ctas: [] } } },
|
|
262
|
+
});
|
|
263
|
+
expect(document.querySelector('.actions')).not.toBeInTheDocument();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('also matches MPUSH channel', () => {
|
|
267
|
+
renderResult({
|
|
268
|
+
channel: 'MPUSH',
|
|
269
|
+
templateData: { androidContent: { title: 'MPUSH Title', message: '' } },
|
|
270
|
+
});
|
|
271
|
+
expect(screen.getByText('MPUSH Title')).toBeInTheDocument();
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// INAPP
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
describe('INAPP', () => {
|
|
279
|
+
it('renders title when present', () => {
|
|
280
|
+
renderResult({ channel: 'INAPP', templateData: { androidContent: { title: 'InApp Title' } } });
|
|
281
|
+
expect(screen.getByText('InApp Title')).toBeInTheDocument();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('does not render title label when title is empty', () => {
|
|
285
|
+
const { container } = render(<>{getContentBody({ channel: 'INAPP', templateData: { androidContent: { title: '' } } })}</>);
|
|
286
|
+
// Fragment renders, but no label for the empty title
|
|
287
|
+
const labels = container.querySelectorAll('[data-testid="cap-label"]');
|
|
288
|
+
const titleLabel = Array.from(labels).find((l) => l.getAttribute('data-type') === 'label2');
|
|
289
|
+
expect(titleLabel).toBeUndefined();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('renders message when present', () => {
|
|
293
|
+
renderResult({ channel: 'INAPP', templateData: { androidContent: { message: 'InApp Msg' } } });
|
|
294
|
+
expect(screen.getByText('InApp Msg')).toBeInTheDocument();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('renders image when present', () => {
|
|
298
|
+
renderResult({
|
|
299
|
+
channel: 'INAPP',
|
|
300
|
+
templateData: {
|
|
301
|
+
androidContent: { expandableDetails: { image: 'http://inapp.png' } },
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
expect(document.querySelector('.mobilepush-image')).toHaveAttribute('src', 'http://inapp.png');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('renders CTAs', () => {
|
|
308
|
+
renderResult({
|
|
309
|
+
channel: 'INAPP',
|
|
310
|
+
templateData: {
|
|
311
|
+
androidContent: {
|
|
312
|
+
expandableDetails: { ctas: [{ actionText: 'Tap' }] },
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
expect(screen.getByText('TAP')).toBeInTheDocument();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('uses iosContent fallback', () => {
|
|
320
|
+
renderResult({
|
|
321
|
+
channel: 'INAPP',
|
|
322
|
+
templateData: { iosContent: { title: 'iOS INAPP' } },
|
|
323
|
+
});
|
|
324
|
+
expect(screen.getByText('iOS INAPP')).toBeInTheDocument();
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
// WEBPUSH
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
describe('WEBPUSH', () => {
|
|
332
|
+
const makeWebpushItem = (innerContent = {}) => ({
|
|
333
|
+
channel: 'WEBPUSH',
|
|
334
|
+
templateData: {
|
|
335
|
+
messageContent: { content: { content: innerContent } },
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('renders title', () => {
|
|
340
|
+
renderResult(makeWebpushItem({ title: 'WP Title' }));
|
|
341
|
+
expect(screen.getByText('WP Title')).toBeInTheDocument();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('renders notification message when present', () => {
|
|
345
|
+
renderResult(makeWebpushItem({ title: 'T', message: 'WP Msg' }));
|
|
346
|
+
expect(screen.getByText('WP Msg')).toBeInTheDocument();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('does not render message div when message is empty', () => {
|
|
350
|
+
const container = renderResult(makeWebpushItem({ title: 'T', message: '' }));
|
|
351
|
+
expect(container.querySelector('.webpush-template-message')).not.toBeInTheDocument();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('renders iconImageUrl as img element', () => {
|
|
355
|
+
renderResult(makeWebpushItem({ iconImageUrl: 'http://icon.png' }));
|
|
356
|
+
const icon = document.querySelector('.webpush-brand-icon');
|
|
357
|
+
expect(icon).toHaveAttribute('src', 'http://icon.png');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('renders placeholder span when iconImageUrl is absent', () => {
|
|
361
|
+
renderResult(makeWebpushItem({}));
|
|
362
|
+
expect(document.querySelector('.webpush-brand-icon-placeholder')).toBeInTheDocument();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('renders media image when expandableDetails.image is set', () => {
|
|
366
|
+
renderResult(makeWebpushItem({ expandableDetails: { image: 'http://media.png' } }));
|
|
367
|
+
expect(document.querySelector('.webpush-media-image')).toHaveAttribute('src', 'http://media.png');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('does not render media image when expandableDetails.image is absent', () => {
|
|
371
|
+
renderResult(makeWebpushItem({}));
|
|
372
|
+
expect(document.querySelector('.webpush-media-image')).not.toBeInTheDocument();
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('renders CTA from single `cta` property', () => {
|
|
376
|
+
renderResult(makeWebpushItem({ cta: { text: 'Buy Now' } }));
|
|
377
|
+
expect(screen.getByText('Buy Now')).toBeInTheDocument();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('renders up to 2 CTAs from ctas array', () => {
|
|
381
|
+
renderResult(makeWebpushItem({
|
|
382
|
+
ctas: [{ text: 'A' }, { text: 'B' }, { text: 'C' }],
|
|
383
|
+
}));
|
|
384
|
+
expect(screen.getByText('A')).toBeInTheDocument();
|
|
385
|
+
expect(screen.getByText('B')).toBeInTheDocument();
|
|
386
|
+
expect(screen.queryByText('C')).not.toBeInTheDocument();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('applies single-cta class when only one CTA', () => {
|
|
390
|
+
renderResult(makeWebpushItem({ cta: { text: 'Solo' } }));
|
|
391
|
+
expect(document.querySelector('.webpush-template-cta-section.single-cta')).toBeInTheDocument();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('does not apply single-cta class when there are 2 CTAs', () => {
|
|
395
|
+
renderResult(makeWebpushItem({ ctas: [{ text: 'A' }, { text: 'B' }] }));
|
|
396
|
+
expect(document.querySelector('.webpush-template-cta-section.single-cta')).not.toBeInTheDocument();
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('renders CTA actionText as fallback when text is absent', () => {
|
|
400
|
+
renderResult(makeWebpushItem({ cta: { actionText: 'FallbackText' } }));
|
|
401
|
+
expect(screen.getByText('FallbackText')).toBeInTheDocument();
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('renders without crash when messageContent is deeply absent', () => {
|
|
405
|
+
renderResult({ channel: 'WEBPUSH', templateData: {} });
|
|
406
|
+
expect(document.querySelector('.webpush-template-card')).toBeInTheDocument();
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
// VIBER
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
describe('VIBER', () => {
|
|
414
|
+
it('renders text from JSON string with content wrapper', () => {
|
|
415
|
+
renderResult({
|
|
416
|
+
channel: 'VIBER',
|
|
417
|
+
templateData: { messageBody: JSON.stringify({ content: { text: 'Viber text' } }) },
|
|
418
|
+
});
|
|
419
|
+
expect(screen.getByText('Viber text')).toBeInTheDocument();
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('renders text from JSON string without content wrapper', () => {
|
|
423
|
+
renderResult({
|
|
424
|
+
channel: 'VIBER',
|
|
425
|
+
templateData: { messageBody: JSON.stringify({ text: 'Direct text' }) },
|
|
426
|
+
});
|
|
427
|
+
expect(screen.getByText('Direct text')).toBeInTheDocument();
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('renders text from object messageBody', () => {
|
|
431
|
+
renderResult({
|
|
432
|
+
channel: 'VIBER',
|
|
433
|
+
templateData: { messageBody: { content: { text: 'Obj text' } } },
|
|
434
|
+
});
|
|
435
|
+
expect(screen.getByText('Obj text')).toBeInTheDocument();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('does not render text label when text is empty', () => {
|
|
439
|
+
const container = renderResult({
|
|
440
|
+
channel: 'VIBER',
|
|
441
|
+
templateData: { messageBody: JSON.stringify({ text: '' }) },
|
|
442
|
+
});
|
|
443
|
+
const labels = container.querySelectorAll('[data-testid="cap-label"]');
|
|
444
|
+
expect(labels).toHaveLength(0);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('renders image when image.url is present', () => {
|
|
448
|
+
renderResult({
|
|
449
|
+
channel: 'VIBER',
|
|
450
|
+
templateData: { messageBody: { image: { url: 'http://viber.png' } } },
|
|
451
|
+
});
|
|
452
|
+
expect(document.querySelector('.viber-image')).toHaveAttribute('src', 'http://viber.png');
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('does not render image when image.url is absent', () => {
|
|
456
|
+
renderResult({
|
|
457
|
+
channel: 'VIBER',
|
|
458
|
+
templateData: { messageBody: { image: {} } },
|
|
459
|
+
});
|
|
460
|
+
expect(document.querySelector('.viber-image')).not.toBeInTheDocument();
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('renders video preview when video.url is present', () => {
|
|
464
|
+
renderResult({
|
|
465
|
+
channel: 'VIBER',
|
|
466
|
+
templateData: { messageBody: { video: { url: 'http://v.mp4', thumbnailUrl: 'http://thumb.png' } } },
|
|
467
|
+
});
|
|
468
|
+
expect(document.querySelector('.video-preview')).toBeInTheDocument();
|
|
469
|
+
const img = document.querySelector('.viber-image');
|
|
470
|
+
expect(img).toHaveAttribute('src', 'http://thumb.png');
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('uses video.url as thumbnail fallback when thumbnailUrl is absent', () => {
|
|
474
|
+
renderResult({
|
|
475
|
+
channel: 'VIBER',
|
|
476
|
+
templateData: { messageBody: { video: { url: 'http://v.mp4' } } },
|
|
477
|
+
});
|
|
478
|
+
const img = document.querySelector('.viber-image');
|
|
479
|
+
expect(img).toHaveAttribute('src', 'http://v.mp4');
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('does not render video when video.url is absent', () => {
|
|
483
|
+
renderResult({
|
|
484
|
+
channel: 'VIBER',
|
|
485
|
+
templateData: { messageBody: { video: {} } },
|
|
486
|
+
});
|
|
487
|
+
expect(document.querySelector('.video-preview')).not.toBeInTheDocument();
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('renders button row when button.text is present', () => {
|
|
491
|
+
renderResult({
|
|
492
|
+
channel: 'VIBER',
|
|
493
|
+
templateData: { messageBody: { button: { text: 'Click' } } },
|
|
494
|
+
});
|
|
495
|
+
expect(screen.getByText('Click')).toBeInTheDocument();
|
|
496
|
+
expect(document.querySelector('.button-content')).toBeInTheDocument();
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('does not render button row when button.text is absent', () => {
|
|
500
|
+
renderResult({
|
|
501
|
+
channel: 'VIBER',
|
|
502
|
+
templateData: { messageBody: { button: {} } },
|
|
503
|
+
});
|
|
504
|
+
expect(document.querySelector('.button-content')).not.toBeInTheDocument();
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('falls back to empty content on JSON parse error', () => {
|
|
508
|
+
renderResult({
|
|
509
|
+
channel: 'VIBER',
|
|
510
|
+
templateData: { messageBody: 'not valid json{{{' },
|
|
511
|
+
});
|
|
512
|
+
// Should not throw; renders empty fragment
|
|
513
|
+
expect(document.querySelector('[data-testid="cap-label"]')).not.toBeInTheDocument();
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('renders without crash when messageBody is absent', () => {
|
|
517
|
+
renderResult({ channel: 'VIBER', templateData: {} });
|
|
518
|
+
expect(document.querySelector('[data-testid="cap-label"]')).not.toBeInTheDocument();
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// ---------------------------------------------------------------------------
|
|
523
|
+
// WHATSAPP
|
|
524
|
+
// ---------------------------------------------------------------------------
|
|
525
|
+
describe('WHATSAPP', () => {
|
|
526
|
+
const makeWA = (configs = {}, extra = {}) => ({
|
|
527
|
+
channel: 'WHATSAPP',
|
|
528
|
+
templateData: { templateConfigs: configs, ...extra },
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('renders bodyText', () => {
|
|
532
|
+
renderResult(makeWA({}, { messageBody: 'WA Body' }));
|
|
533
|
+
expect(screen.getByText('WA Body')).toBeInTheDocument();
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('renders IMAGE type', () => {
|
|
537
|
+
renderResult(makeWA({ mediaType: 'IMAGE', whatsappMedia: { url: 'http://wa.png' } }));
|
|
538
|
+
expect(document.querySelector('.whatsapp-image')).toHaveAttribute('src', 'http://wa.png');
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it('does not render direct image element when mediaType is VIDEO', () => {
|
|
542
|
+
// VIDEO path renders a .video-preview div, not a bare .whatsapp-image from the imageUrl branch
|
|
543
|
+
renderResult(makeWA({ mediaType: 'VIDEO', whatsappMedia: { previewUrl: 'http://preview.png' } }));
|
|
544
|
+
// No direct .whatsapp-image outside of .video-preview for the top-level imageUrl === ''
|
|
545
|
+
const images = document.querySelectorAll('.whatsapp-container > .whatsapp-image');
|
|
546
|
+
expect(images).toHaveLength(0);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it('renders video preview for VIDEO mediaType', () => {
|
|
550
|
+
renderResult(makeWA({ mediaType: 'VIDEO', whatsappMedia: { previewUrl: 'http://preview.png' } }));
|
|
551
|
+
expect(document.querySelector('.video-preview')).toBeInTheDocument();
|
|
552
|
+
const img = document.querySelector('.whatsapp-image');
|
|
553
|
+
expect(img).toHaveAttribute('src', 'http://preview.png');
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it('renders doc preview for DOCUMENT mediaType', () => {
|
|
557
|
+
renderResult(makeWA({
|
|
558
|
+
mediaType: 'DOCUMENT',
|
|
559
|
+
whatsappMedia: { docParams: { fileName: 'file.pdf' }, previewUrl: 'http://doc.png' },
|
|
560
|
+
}));
|
|
561
|
+
expect(screen.getByTestId('doc-preview')).toBeInTheDocument();
|
|
562
|
+
expect(getWhatsappDocPreview).toHaveBeenCalledWith(
|
|
563
|
+
expect.objectContaining({ fileName: 'file.pdf', whatsappDocImg: 'http://doc.png' }),
|
|
564
|
+
);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('does not call getWhatsappDocPreview when docParams is absent', () => {
|
|
568
|
+
renderResult(makeWA({ mediaType: 'DOCUMENT', whatsappMedia: {} }));
|
|
569
|
+
expect(getWhatsappDocPreview).not.toHaveBeenCalled();
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it('renders header inside whatsapp-message span', () => {
|
|
573
|
+
renderResult(makeWA({ whatsappMedia: { header: 'Header Text' } }));
|
|
574
|
+
expect(screen.getByText('Header Text')).toBeInTheDocument();
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('renders footer inside whatsapp-message span', () => {
|
|
578
|
+
renderResult(makeWA({ whatsappMedia: { footer: 'Footer Text' } }));
|
|
579
|
+
expect(screen.getByText('Footer Text')).toBeInTheDocument();
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
it('applies whatsapp-message-with-media class when imageUrl is set', () => {
|
|
583
|
+
renderResult(makeWA({ mediaType: 'IMAGE', whatsappMedia: { url: 'http://img.png' } }));
|
|
584
|
+
expect(document.querySelector('.whatsapp-message-with-media')).toBeInTheDocument();
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('applies whatsapp-message-without-media class when no media', () => {
|
|
588
|
+
renderResult(makeWA({}));
|
|
589
|
+
expect(document.querySelector('.whatsapp-message-without-media')).toBeInTheDocument();
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it('renders CTA buttons with PHONE_NUMBER type using call icon', () => {
|
|
593
|
+
renderResult(makeWA({
|
|
594
|
+
buttonType: 'CTA',
|
|
595
|
+
buttons: [{ text: 'Call Us', type: 'PHONE_NUMBER' }],
|
|
596
|
+
}));
|
|
597
|
+
expect(screen.getByText('Call Us')).toBeInTheDocument();
|
|
598
|
+
expect(document.querySelector('[data-testid="cap-icon-call"]')).toBeInTheDocument();
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('renders CTA buttons with URL type using launch icon', () => {
|
|
602
|
+
renderResult(makeWA({
|
|
603
|
+
buttonType: 'CTA',
|
|
604
|
+
buttons: [{ text: 'Visit', type: 'URL' }],
|
|
605
|
+
}));
|
|
606
|
+
expect(screen.getByText('Visit')).toBeInTheDocument();
|
|
607
|
+
expect(document.querySelector('[data-testid="cap-icon-launch"]')).toBeInTheDocument();
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it('skips CTA button when button.text is absent', () => {
|
|
611
|
+
renderResult(makeWA({
|
|
612
|
+
buttonType: 'CTA',
|
|
613
|
+
buttons: [{ type: 'URL' }],
|
|
614
|
+
}));
|
|
615
|
+
expect(document.querySelector('[data-testid="cap-icon-launch"]')).not.toBeInTheDocument();
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('does not render CTA buttons when buttonType is not CTA', () => {
|
|
619
|
+
renderResult(makeWA({ buttonType: 'QUICK_REPLY', buttons: [{ text: 'Reply' }] }));
|
|
620
|
+
expect(screen.queryByText('Reply')).not.toBeInTheDocument();
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('calls getWhatsappQuickReply with buttonType and buttons', () => {
|
|
624
|
+
renderResult(makeWA({ buttonType: 'QUICK_REPLY', buttons: [{ text: 'Yes' }] }));
|
|
625
|
+
expect(getWhatsappQuickReply).toHaveBeenCalledWith(
|
|
626
|
+
{ buttonType: 'QUICK_REPLY', buttons: [{ text: 'Yes' }] },
|
|
627
|
+
true,
|
|
628
|
+
);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it('renders carousel with image mediaType', () => {
|
|
632
|
+
renderResult(makeWA({ cards: [{ mediaType: 'IMAGE', imageUrl: 'http://c.png', bodyText: 'Card 1', buttons: [] }] }));
|
|
633
|
+
expect(document.querySelector('.scroll-container')).toBeInTheDocument();
|
|
634
|
+
expect(document.querySelector('.whatsapp-image')).toHaveAttribute('src', 'http://c.png');
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it('renders carousel with video mediaType', () => {
|
|
638
|
+
renderResult(makeWA({ cards: [{ mediaType: 'VIDEO', videoPreviewImg: 'http://vp.png', buttons: [] }] }));
|
|
639
|
+
expect(document.querySelector('.video-preview')).toBeInTheDocument();
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it('applies whatsapp-message-with-media on carousel card when imageUrl is present', () => {
|
|
643
|
+
renderResult(makeWA({ cards: [{ mediaType: 'IMAGE', imageUrl: 'http://c.png', buttons: [] }] }));
|
|
644
|
+
expect(document.querySelector('.whatsapp-carousel-card .whatsapp-message-with-media')).toBeInTheDocument();
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it('applies whatsapp-message-without-media on carousel card when no media', () => {
|
|
648
|
+
renderResult(makeWA({ cards: [{ buttons: [] }] }));
|
|
649
|
+
expect(document.querySelector('.whatsapp-carousel-card .whatsapp-message-without-media')).toBeInTheDocument();
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it('calls getWhatsappCarouselButtonView for each carousel card', () => {
|
|
653
|
+
renderResult(makeWA({
|
|
654
|
+
cards: [
|
|
655
|
+
{ buttons: [{ text: 'Btn1' }] },
|
|
656
|
+
{ buttons: [{ text: 'Btn2' }] },
|
|
657
|
+
],
|
|
658
|
+
}));
|
|
659
|
+
expect(getWhatsappCarouselButtonView).toHaveBeenCalledTimes(2);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it('renders without crash when templateConfigs is absent', () => {
|
|
663
|
+
renderResult({ channel: 'WHATSAPP', templateData: {} });
|
|
664
|
+
expect(document.querySelector('.whatsapp-container')).toBeInTheDocument();
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// ---------------------------------------------------------------------------
|
|
669
|
+
// RCS
|
|
670
|
+
// ---------------------------------------------------------------------------
|
|
671
|
+
describe('RCS', () => {
|
|
672
|
+
const makeRCS = (cardContent = {}) => ({
|
|
673
|
+
channel: 'RCS',
|
|
674
|
+
templateData: { rcsContent: { cardContent: [cardContent] } },
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it('renders IMAGE media', () => {
|
|
678
|
+
renderResult(makeRCS({ media: { mediaType: 'IMAGE', mediaUrl: 'http://rcs.png' } }));
|
|
679
|
+
expect(document.querySelector('.rcs-image')).toHaveAttribute('src', 'http://rcs.png');
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it('renders VIDEO media with thumbnailUrl', () => {
|
|
683
|
+
renderResult(makeRCS({ media: { mediaType: 'VIDEO', mediaUrl: 'http://v.mp4', thumbnailUrl: 'http://t.png' } }));
|
|
684
|
+
expect(document.querySelector('.video-preview')).toBeInTheDocument();
|
|
685
|
+
expect(document.querySelector('.rcs-image')).toHaveAttribute('src', 'http://t.png');
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it('renders VIDEO media using mediaUrl when thumbnailUrl is absent', () => {
|
|
689
|
+
renderResult(makeRCS({ media: { mediaType: 'VIDEO', mediaUrl: 'http://v.mp4' } }));
|
|
690
|
+
expect(document.querySelector('.rcs-image')).toHaveAttribute('src', 'http://v.mp4');
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
it('does not render image for unknown mediaType', () => {
|
|
694
|
+
renderResult(makeRCS({ media: { mediaType: 'DOCUMENT', mediaUrl: 'http://d.pdf' } }));
|
|
695
|
+
expect(document.querySelector('.rcs-image')).not.toBeInTheDocument();
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it('renders title when present', () => {
|
|
699
|
+
renderResult(makeRCS({ title: 'RCS Title' }));
|
|
700
|
+
expect(screen.getByText('RCS Title')).toBeInTheDocument();
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it('does not render title label when title is empty', () => {
|
|
704
|
+
renderResult(makeRCS({ title: '' }));
|
|
705
|
+
const labels = document.querySelectorAll('[data-testid="cap-label"][data-type="label4"]');
|
|
706
|
+
expect(labels).toHaveLength(0);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it('renders description when present', () => {
|
|
710
|
+
renderResult(makeRCS({ description: 'RCS Desc' }));
|
|
711
|
+
expect(screen.getByText('RCS Desc')).toBeInTheDocument();
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it('renders suggestions as action spans', () => {
|
|
715
|
+
renderResult(makeRCS({
|
|
716
|
+
suggestions: [
|
|
717
|
+
{ label: 'Yes' },
|
|
718
|
+
{ text: 'No' },
|
|
719
|
+
{ postback: { data: 'pb-data' } },
|
|
720
|
+
],
|
|
721
|
+
}));
|
|
722
|
+
expect(screen.getByText('Yes')).toBeInTheDocument();
|
|
723
|
+
expect(screen.getByText('No')).toBeInTheDocument();
|
|
724
|
+
// suggestion with only postback — label and text are both undefined, renders empty span
|
|
725
|
+
expect(document.querySelectorAll('.action')).toHaveLength(3);
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it('does not render actions div when suggestions is empty', () => {
|
|
729
|
+
renderResult(makeRCS({ suggestions: [] }));
|
|
730
|
+
expect(document.querySelector('.actions')).not.toBeInTheDocument();
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it('renders without crash when rcsContent is absent', () => {
|
|
734
|
+
renderResult({ channel: 'RCS', templateData: {} });
|
|
735
|
+
expect(document.querySelector('[data-testid="cap-label"]')).not.toBeInTheDocument();
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
// ---------------------------------------------------------------------------
|
|
740
|
+
// ZALO
|
|
741
|
+
// ---------------------------------------------------------------------------
|
|
742
|
+
describe('ZALO', () => {
|
|
743
|
+
it('renders zalo template name', () => {
|
|
744
|
+
renderResult({
|
|
745
|
+
channel: 'ZALO',
|
|
746
|
+
templateData: { templateConfigs: { name: 'Zalo Promo' } },
|
|
747
|
+
});
|
|
748
|
+
const label = document.querySelector('[data-testid="cap-label"].zalo-listing-content');
|
|
749
|
+
expect(label).toHaveTextContent('Zalo Promo');
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
it('renders empty label when name is absent', () => {
|
|
753
|
+
renderResult({ channel: 'ZALO', templateData: { templateConfigs: {} } });
|
|
754
|
+
const label = document.querySelector('[data-testid="cap-label"].zalo-listing-content');
|
|
755
|
+
expect(label).toHaveTextContent('');
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
it('renders without crash when templateConfigs is absent', () => {
|
|
759
|
+
renderResult({ channel: 'ZALO', templateData: {} });
|
|
760
|
+
expect(document.querySelector('[data-testid="cap-label"]')).toBeInTheDocument();
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
// ---------------------------------------------------------------------------
|
|
765
|
+
// LINE
|
|
766
|
+
// ---------------------------------------------------------------------------
|
|
767
|
+
describe('LINE', () => {
|
|
768
|
+
const makeLine = (messages) => ({
|
|
769
|
+
channel: 'LINE',
|
|
770
|
+
templateData: {
|
|
771
|
+
messageBody: JSON.stringify({ messages }),
|
|
772
|
+
},
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it('renders flex type using contents[0].hero.url', () => {
|
|
776
|
+
renderResult(makeLine([{
|
|
777
|
+
type: 'flex',
|
|
778
|
+
contents: {
|
|
779
|
+
contents: [{ hero: { url: 'http://flex.png' } }],
|
|
780
|
+
},
|
|
781
|
+
}]));
|
|
782
|
+
expect(document.querySelector('.line-image')).toHaveAttribute('src', 'http://flex.png');
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it('renders flex type using contents.hero.url as fallback', () => {
|
|
786
|
+
renderResult(makeLine([{
|
|
787
|
+
type: 'flex',
|
|
788
|
+
contents: {
|
|
789
|
+
hero: { url: 'http://flex-hero.png' },
|
|
790
|
+
contents: [],
|
|
791
|
+
},
|
|
792
|
+
}]));
|
|
793
|
+
expect(document.querySelector('.line-image')).toHaveAttribute('src', 'http://flex-hero.png');
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
it('returns null for flex type when no hero url is found', () => {
|
|
797
|
+
const result = getContentBody(makeLine([{ type: 'flex', contents: { contents: [] } }]));
|
|
798
|
+
const { container } = render(<>{result}</>);
|
|
799
|
+
expect(container.firstChild).toBeNull();
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
it('renders template type with columns', () => {
|
|
803
|
+
renderResult(makeLine([{
|
|
804
|
+
type: 'template',
|
|
805
|
+
template: {
|
|
806
|
+
columns: [
|
|
807
|
+
{ imageUrl: 'http://col1.png', action: { label: 'Buy' } },
|
|
808
|
+
{ imageUrl: 'http://col2.png', action: { label: 'Skip' } },
|
|
809
|
+
],
|
|
810
|
+
},
|
|
811
|
+
}]));
|
|
812
|
+
const imgs = document.querySelectorAll('img');
|
|
813
|
+
expect(imgs).toHaveLength(2);
|
|
814
|
+
expect(imgs[0]).toHaveAttribute('src', 'http://col1.png');
|
|
815
|
+
expect(imgs[0]).toHaveAttribute('alt', 'Buy');
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
it('renders video type when originalContentUrl is set', () => {
|
|
819
|
+
renderResult(makeLine([{
|
|
820
|
+
type: 'video',
|
|
821
|
+
video: { originalContentUrl: 'http://line-video.mp4' },
|
|
822
|
+
}]));
|
|
823
|
+
expect(document.querySelector('.video-preview')).toBeInTheDocument();
|
|
824
|
+
expect(document.querySelector('source')).toHaveAttribute('src', 'http://line-video.mp4');
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
it('returns null for video type when originalContentUrl is empty', () => {
|
|
828
|
+
const result = getContentBody(makeLine([{ type: 'video', video: { originalContentUrl: '' } }]));
|
|
829
|
+
const { container } = render(<>{result}</>);
|
|
830
|
+
expect(container.firstChild).toBeNull();
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
it('renders image type using originalContentUrl', () => {
|
|
834
|
+
renderResult(makeLine([{ type: 'image', originalContentUrl: 'http://line-img.png' }]));
|
|
835
|
+
expect(document.querySelector('.line-image')).toHaveAttribute('src', 'http://line-img.png');
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
it('renders sticker using animatedStickerUrl', () => {
|
|
839
|
+
renderResult(makeLine([{ type: 'sticker', animatedStickerUrl: 'http://asticker.gif' }]));
|
|
840
|
+
expect(document.querySelector('.line-image')).toHaveAttribute('src', 'http://asticker.gif');
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
it('renders sticker using stickerUrl fallback', () => {
|
|
844
|
+
renderResult(makeLine([{ type: 'sticker', stickerUrl: 'http://sticker.png' }]));
|
|
845
|
+
expect(document.querySelector('.line-image')).toHaveAttribute('src', 'http://sticker.png');
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
it('renders imagemap using baseUrl + /1040', () => {
|
|
849
|
+
renderResult(makeLine([{ type: 'imagemap', baseUrl: 'http://base' }]));
|
|
850
|
+
expect(document.querySelector('.line-image')).toHaveAttribute('src', 'http://base/1040');
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it('renders text message', () => {
|
|
854
|
+
renderResult(makeLine([{ type: 'text', text: 'Hello LINE' }]));
|
|
855
|
+
expect(screen.getByText('Hello LINE')).toBeInTheDocument();
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
it('returns null when message has no renderable content', () => {
|
|
859
|
+
const result = getContentBody(makeLine([{ type: 'text' }]));
|
|
860
|
+
const { container } = render(<>{result}</>);
|
|
861
|
+
expect(container.firstChild).toBeNull();
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
it('handles messageBody as an object (not string)', () => {
|
|
865
|
+
renderResult({
|
|
866
|
+
channel: 'LINE',
|
|
867
|
+
templateData: {
|
|
868
|
+
messageBody: { messages: [{ type: 'text', text: 'Obj LINE' }] },
|
|
869
|
+
},
|
|
870
|
+
});
|
|
871
|
+
expect(screen.getByText('Obj LINE')).toBeInTheDocument();
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
it('falls back to empty content on JSON parse error', () => {
|
|
875
|
+
renderResult({ channel: 'LINE', templateData: { messageBody: '{invalid' } });
|
|
876
|
+
// No messages → firstMessage is {} → returns null
|
|
877
|
+
const result = getContentBody({ channel: 'LINE', templateData: { messageBody: '{invalid' } });
|
|
878
|
+
const { container } = render(<>{result}</>);
|
|
879
|
+
expect(container.firstChild).toBeNull();
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
it('renders without crash when messageBody is absent', () => {
|
|
883
|
+
renderResult({ channel: 'LINE', templateData: {} });
|
|
884
|
+
});
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
// ---------------------------------------------------------------------------
|
|
888
|
+
// default
|
|
889
|
+
// ---------------------------------------------------------------------------
|
|
890
|
+
describe('default (unknown channel)', () => {
|
|
891
|
+
it('renders messageBody for unknown channel', () => {
|
|
892
|
+
renderResult({ channel: 'TELEGRAM', templateData: { messageBody: 'Telegram msg' } });
|
|
893
|
+
expect(screen.getByText('Telegram msg')).toBeInTheDocument();
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
it('falls back to message when messageBody is absent', () => {
|
|
897
|
+
renderResult({ channel: 'TELEGRAM', templateData: { message: 'Alt msg' } });
|
|
898
|
+
expect(screen.getByText('Alt msg')).toBeInTheDocument();
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
it('falls back to item.channel when both are absent', () => {
|
|
902
|
+
renderResult({ channel: 'TELEGRAM', templateData: {} });
|
|
903
|
+
expect(screen.getByText('TELEGRAM')).toBeInTheDocument();
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
it('renders empty string when channel, messageBody, message are all absent', () => {
|
|
907
|
+
renderResult({ channel: undefined, templateData: {} });
|
|
908
|
+
const label = document.querySelector('[data-testid="cap-label"]');
|
|
909
|
+
expect(label).toHaveTextContent('');
|
|
910
|
+
});
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
// ---------------------------------------------------------------------------
|
|
914
|
+
// null / undefined item (optional chaining)
|
|
915
|
+
// ---------------------------------------------------------------------------
|
|
916
|
+
describe('null/undefined item safety', () => {
|
|
917
|
+
// item?.templateData and item?.channel?.toUpperCase() guard the early extraction,
|
|
918
|
+
// but the default branch still reads item.channel directly — so null/undefined throws.
|
|
919
|
+
it('throws for null item (default branch reads item.channel without ?.)', () => {
|
|
920
|
+
expect(() => getContentBody(null)).toThrow(TypeError);
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
it('throws for undefined item (default branch reads item.channel without ?.)', () => {
|
|
924
|
+
expect(() => getContentBody(undefined)).toThrow(TypeError);
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
it('does not crash when templateData is absent on item', () => {
|
|
928
|
+
const result = getContentBody({ channel: 'SMS' });
|
|
929
|
+
const { container } = render(<>{result}</>);
|
|
930
|
+
expect(container.querySelector('[data-testid="cap-label"]')).toHaveTextContent('');
|
|
931
|
+
});
|
|
932
|
+
});
|
|
933
|
+
});
|