Skip to content

Commit 2f06661

Browse files
committed
test: add unit tests for IterableEmbeddedBanner component functionality
1 parent a22a039 commit 2f06661

1 file changed

Lines changed: 372 additions & 0 deletions

File tree

Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { fireEvent, render } from '@testing-library/react-native';
3+
4+
import { IterableEmbeddedViewType } from '../../enums/IterableEmbeddedViewType';
5+
import { useEmbeddedView } from '../../hooks/useEmbeddedView';
6+
import type { IterableEmbeddedMessage } from '../../types/IterableEmbeddedMessage';
7+
import type { IterableEmbeddedMessageElementsButton } from '../../types/IterableEmbeddedMessageElementsButton';
8+
import { IterableEmbeddedBanner } from './IterableEmbeddedBanner';
9+
10+
const mockHandleButtonClick = jest.fn();
11+
const mockHandleMessageClick = jest.fn();
12+
13+
jest.mock('../../hooks/useEmbeddedView', () => ({
14+
useEmbeddedView: jest.fn(),
15+
}));
16+
17+
const mockUseEmbeddedView = useEmbeddedView as jest.MockedFunction<
18+
typeof useEmbeddedView
19+
>;
20+
21+
const defaultParsedStyles = {
22+
backgroundColor: '#ffffff',
23+
borderColor: '#E0DEDF',
24+
borderCornerRadius: 8,
25+
borderWidth: 1,
26+
primaryBtnBackgroundColor: '#6A266D',
27+
primaryBtnTextColor: '#ffffff',
28+
secondaryBtnBackgroundColor: 'transparent',
29+
secondaryBtnTextColor: '#79347F',
30+
titleTextColor: '#3D3A3B',
31+
bodyTextColor: '#787174',
32+
};
33+
34+
function mockUseEmbeddedViewReturn(
35+
overrides: Partial<ReturnType<typeof useEmbeddedView>> = {}
36+
) {
37+
mockUseEmbeddedView.mockReturnValue({
38+
parsedStyles: defaultParsedStyles,
39+
handleButtonClick: mockHandleButtonClick,
40+
handleMessageClick: mockHandleMessageClick,
41+
media: { url: null, caption: null, shouldShow: false },
42+
...overrides,
43+
});
44+
}
45+
46+
describe('IterableEmbeddedBanner', () => {
47+
const baseMessage: IterableEmbeddedMessage = {
48+
metadata: {
49+
messageId: 'msg-1',
50+
campaignId: 1,
51+
placementId: 1,
52+
},
53+
elements: {
54+
title: 'Banner Title',
55+
body: 'Banner body text.',
56+
},
57+
};
58+
59+
beforeEach(() => {
60+
jest.clearAllMocks();
61+
mockUseEmbeddedViewReturn();
62+
});
63+
64+
describe('Rendering', () => {
65+
it('should render without crashing', () => {
66+
const { getByText } = render(
67+
<IterableEmbeddedBanner message={baseMessage} />
68+
);
69+
expect(getByText('Banner Title')).toBeTruthy();
70+
expect(getByText('Banner body text.')).toBeTruthy();
71+
});
72+
73+
it('should render title and body from message.elements', () => {
74+
const message: IterableEmbeddedMessage = {
75+
...baseMessage,
76+
elements: {
77+
title: 'Custom Banner Title',
78+
body: 'Custom banner body.',
79+
},
80+
};
81+
const { getByText } = render(
82+
<IterableEmbeddedBanner message={message} />
83+
);
84+
expect(getByText('Custom Banner Title')).toBeTruthy();
85+
expect(getByText('Custom banner body.')).toBeTruthy();
86+
});
87+
88+
it('should apply parsedStyles to container and text', () => {
89+
const customStyles = {
90+
...defaultParsedStyles,
91+
backgroundColor: '#000000',
92+
titleTextColor: '#ff0000',
93+
bodyTextColor: '#00ff00',
94+
};
95+
mockUseEmbeddedViewReturn({ parsedStyles: customStyles });
96+
97+
const { getByText, UNSAFE_getAllByType } = render(
98+
<IterableEmbeddedBanner message={baseMessage} />
99+
);
100+
101+
const views = UNSAFE_getAllByType('View' as any);
102+
const styleArray = (s: any) => (Array.isArray(s) ? s : [s]);
103+
const container = views.find(
104+
(v: any) =>
105+
v.props.style &&
106+
styleArray(v.props.style).some(
107+
(sty: any) => sty && sty.backgroundColor === '#000000'
108+
)
109+
);
110+
expect(container).toBeTruthy();
111+
expect(styleArray(container!.props.style)).toEqual(
112+
expect.arrayContaining([
113+
expect.any(Object),
114+
expect.objectContaining({
115+
backgroundColor: '#000000',
116+
borderColor: customStyles.borderColor,
117+
borderRadius: customStyles.borderCornerRadius,
118+
borderWidth: customStyles.borderWidth,
119+
}),
120+
])
121+
);
122+
123+
const title = getByText('Banner Title');
124+
const body = getByText('Banner body text.');
125+
expect(title.props.style).toEqual(
126+
expect.arrayContaining([
127+
expect.any(Object),
128+
expect.objectContaining({ color: '#ff0000' }),
129+
])
130+
);
131+
expect(body.props.style).toEqual(
132+
expect.arrayContaining([
133+
expect.any(Object),
134+
expect.objectContaining({ color: '#00ff00' }),
135+
])
136+
);
137+
});
138+
139+
it('should not render button container when message has no buttons', () => {
140+
const message: IterableEmbeddedMessage = {
141+
...baseMessage,
142+
elements: { ...baseMessage.elements, buttons: undefined },
143+
};
144+
const { queryByText } = render(
145+
<IterableEmbeddedBanner message={message} />
146+
);
147+
expect(queryByText('Primary')).toBeNull();
148+
});
149+
150+
it('should not render button container when buttons array is empty', () => {
151+
const message: IterableEmbeddedMessage = {
152+
...baseMessage,
153+
elements: { ...baseMessage.elements, buttons: [] },
154+
};
155+
const { queryByText } = render(
156+
<IterableEmbeddedBanner message={message} />
157+
);
158+
expect(queryByText('Primary')).toBeNull();
159+
});
160+
});
161+
162+
describe('Buttons', () => {
163+
const primaryButton: IterableEmbeddedMessageElementsButton = {
164+
id: 'btn-primary',
165+
title: 'Primary',
166+
action: { type: 'openUrl', data: 'https://example.com' },
167+
};
168+
const secondaryButton: IterableEmbeddedMessageElementsButton = {
169+
id: 'btn-secondary',
170+
title: 'Secondary',
171+
};
172+
173+
it('should render buttons when message has buttons', () => {
174+
const message: IterableEmbeddedMessage = {
175+
...baseMessage,
176+
elements: {
177+
...baseMessage.elements,
178+
buttons: [primaryButton, secondaryButton],
179+
},
180+
};
181+
const { getByText } = render(
182+
<IterableEmbeddedBanner message={message} />
183+
);
184+
expect(getByText('Primary')).toBeTruthy();
185+
expect(getByText('Secondary')).toBeTruthy();
186+
});
187+
188+
it('should apply primary and secondary button text colors from parsedStyles', () => {
189+
const message: IterableEmbeddedMessage = {
190+
...baseMessage,
191+
elements: {
192+
...baseMessage.elements,
193+
buttons: [primaryButton, secondaryButton],
194+
},
195+
};
196+
const { getByText } = render(
197+
<IterableEmbeddedBanner message={message} />
198+
);
199+
200+
const primaryText = getByText('Primary');
201+
const secondaryText = getByText('Secondary');
202+
expect(primaryText.props.style).toEqual(
203+
expect.arrayContaining([
204+
expect.any(Object),
205+
expect.objectContaining({
206+
color: defaultParsedStyles.primaryBtnTextColor,
207+
}),
208+
])
209+
);
210+
expect(secondaryText.props.style).toEqual(
211+
expect.arrayContaining([
212+
expect.any(Object),
213+
expect.objectContaining({
214+
color: defaultParsedStyles.secondaryBtnTextColor,
215+
}),
216+
])
217+
);
218+
});
219+
220+
it('should call handleButtonClick with correct button when button is pressed', () => {
221+
const message: IterableEmbeddedMessage = {
222+
...baseMessage,
223+
elements: {
224+
...baseMessage.elements,
225+
buttons: [primaryButton, secondaryButton],
226+
},
227+
};
228+
const { getByText } = render(
229+
<IterableEmbeddedBanner message={message} />
230+
);
231+
232+
fireEvent.press(getByText('Primary'));
233+
expect(mockHandleButtonClick).toHaveBeenCalledTimes(1);
234+
expect(mockHandleButtonClick).toHaveBeenCalledWith(primaryButton);
235+
236+
fireEvent.press(getByText('Secondary'));
237+
expect(mockHandleButtonClick).toHaveBeenCalledTimes(2);
238+
expect(mockHandleButtonClick).toHaveBeenLastCalledWith(secondaryButton);
239+
});
240+
});
241+
242+
describe('Media', () => {
243+
it('should not render media when media.shouldShow is false', () => {
244+
const { UNSAFE_queryAllByType } = render(
245+
<IterableEmbeddedBanner message={baseMessage} />
246+
);
247+
const images = UNSAFE_queryAllByType('Image' as any);
248+
expect(images.length).toBe(0);
249+
});
250+
251+
it('should render media image when media.shouldShow is true', () => {
252+
const media = {
253+
url: 'https://example.com/image.png',
254+
caption: 'Banner image',
255+
shouldShow: true,
256+
};
257+
mockUseEmbeddedViewReturn({ media });
258+
259+
const { UNSAFE_queryAllByType } = render(
260+
<IterableEmbeddedBanner message={baseMessage} />
261+
);
262+
263+
const images = UNSAFE_queryAllByType('Image' as any);
264+
expect(images.length).toBeGreaterThan(0);
265+
expect((images[0] as any).props.source.uri).toBe(media.url);
266+
});
267+
});
268+
269+
describe('Message click', () => {
270+
it('should call handleMessageClick when banner is pressed', () => {
271+
const { getByText } = render(
272+
<IterableEmbeddedBanner message={baseMessage} />
273+
);
274+
275+
fireEvent.press(getByText('Banner Title'));
276+
expect(mockHandleMessageClick).toHaveBeenCalledTimes(1);
277+
});
278+
});
279+
280+
describe('useEmbeddedView integration', () => {
281+
it('should call useEmbeddedView with Banner viewType and props', () => {
282+
const config = { backgroundColor: '#abc' } as any;
283+
const onButtonClick = jest.fn();
284+
const onMessageClick = jest.fn();
285+
286+
render(
287+
<IterableEmbeddedBanner
288+
message={baseMessage}
289+
config={config}
290+
onButtonClick={onButtonClick}
291+
onMessageClick={onMessageClick}
292+
/>
293+
);
294+
295+
expect(mockUseEmbeddedView).toHaveBeenCalledTimes(1);
296+
expect(mockUseEmbeddedView).toHaveBeenCalledWith(
297+
IterableEmbeddedViewType.Banner,
298+
{
299+
message: baseMessage,
300+
config,
301+
onButtonClick,
302+
onMessageClick,
303+
}
304+
);
305+
});
306+
307+
it('should call useEmbeddedView with default callbacks when not provided', () => {
308+
render(<IterableEmbeddedBanner message={baseMessage} />);
309+
310+
expect(mockUseEmbeddedView).toHaveBeenCalledWith(
311+
IterableEmbeddedViewType.Banner,
312+
expect.objectContaining({
313+
message: baseMessage,
314+
onButtonClick: expect.any(Function),
315+
onMessageClick: expect.any(Function),
316+
})
317+
);
318+
});
319+
});
320+
321+
describe('Edge cases', () => {
322+
it('should handle message with missing elements', () => {
323+
const message: IterableEmbeddedMessage = {
324+
metadata: baseMessage.metadata,
325+
elements: undefined,
326+
};
327+
const { queryByText } = render(
328+
<IterableEmbeddedBanner message={message} />
329+
);
330+
expect(queryByText('Banner Title')).toBeNull();
331+
expect(queryByText('Banner body text.')).toBeNull();
332+
});
333+
334+
it('should handle message with empty title and body without throwing', () => {
335+
const message: IterableEmbeddedMessage = {
336+
...baseMessage,
337+
elements: { title: '', body: '' },
338+
};
339+
const { getAllByText } = render(
340+
<IterableEmbeddedBanner message={message} />
341+
);
342+
const emptyTextNodes = getAllByText('');
343+
expect(emptyTextNodes.length).toBeGreaterThanOrEqual(1);
344+
});
345+
346+
it('should render multiple buttons and call handleButtonClick with correct button for each', () => {
347+
const message: IterableEmbeddedMessage = {
348+
...baseMessage,
349+
elements: {
350+
...baseMessage.elements,
351+
buttons: [
352+
{ id: 'unique-id-1', title: 'First' },
353+
{ id: 'unique-id-2', title: 'Second' },
354+
],
355+
},
356+
};
357+
const { getByText } = render(
358+
<IterableEmbeddedBanner message={message} />
359+
);
360+
expect(getByText('First')).toBeTruthy();
361+
expect(getByText('Second')).toBeTruthy();
362+
fireEvent.press(getByText('First'));
363+
expect(mockHandleButtonClick).toHaveBeenCalledWith(
364+
expect.objectContaining({ id: 'unique-id-1', title: 'First' })
365+
);
366+
fireEvent.press(getByText('Second'));
367+
expect(mockHandleButtonClick).toHaveBeenLastCalledWith(
368+
expect.objectContaining({ id: 'unique-id-2', title: 'Second' })
369+
);
370+
});
371+
});
372+
});

0 commit comments

Comments
 (0)