Skip to content

Commit 4469706

Browse files
committed
test(carousel): improve test coverage
HH-380
1 parent 1e6902f commit 4469706

4 files changed

Lines changed: 570 additions & 0 deletions

File tree

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import React from 'react';
2+
import { render, screen, act } from '@testing-library/react';
3+
import '@testing-library/jest-dom';
4+
5+
import { Carousel } from '../Carousel';
6+
import { MOBILE_WIDTH } from '../constants';
7+
8+
// Mock child components to isolate the Carousel's own logic
9+
jest.mock('../components/CarouselSlider', () => ({
10+
CarouselSlider: ({
11+
children,
12+
}: {
13+
children: React.ReactElement<unknown>[];
14+
}) => <div data-testid="carousel-slider">{children}</div>,
15+
}));
16+
17+
jest.mock('../components/CarouselSlideButton', () => ({
18+
CarouselPreviousSlideButton: () => <button type="button">Previous</button>,
19+
CarouselNextSlideButton: () => <button type="button">Next</button>,
20+
}));
21+
22+
jest.mock('../components/CarouselSliderDot', () => ({
23+
CarouselSlideDots: () => <div data-testid="carousel-dots" />,
24+
}));
25+
26+
// Mock translation hook
27+
jest.mock('../../translation/useTranslationWithFallback', () => ({
28+
useTranslationWithFallback: () => ({
29+
t: (key: string) => {
30+
if (key === 'carouselRegionLabelText') {
31+
return 'Carousel: {title}';
32+
}
33+
return key;
34+
},
35+
}),
36+
}));
37+
38+
const mockItems = [
39+
<div key="1">Item 1</div>,
40+
<div key="2">Item 2</div>,
41+
<div key="3">Item 3</div>,
42+
<div key="4">Item 4</div>,
43+
<div key="5">Item 5</div>,
44+
];
45+
46+
// Helper to set window width and fire resize event
47+
const fireResize = (width: number) => {
48+
act(() => {
49+
window.innerWidth = width;
50+
window.dispatchEvent(new Event('resize'));
51+
});
52+
};
53+
54+
describe('Carousel', () => {
55+
const originalInnerWidth = window.innerWidth;
56+
57+
afterEach(() => {
58+
window.innerWidth = originalInnerWidth;
59+
});
60+
61+
it('renders children and the slider', () => {
62+
render(<Carousel>{mockItems}</Carousel>);
63+
expect(screen.getByTestId('carousel-slider')).toBeInTheDocument();
64+
expect(screen.getByText('Item 1')).toBeInTheDocument();
65+
expect(screen.getByText('Item 2')).toBeInTheDocument();
66+
expect(screen.getByText('Item 3')).toBeInTheDocument();
67+
expect(screen.getByText('Item 4')).toBeInTheDocument();
68+
expect(screen.getByText('Item 5')).toBeInTheDocument();
69+
});
70+
71+
it('renders with a correct ARIA region label', () => {
72+
const title = 'My Test Carousel';
73+
render(<Carousel title={title}>{mockItems}</Carousel>);
74+
expect(
75+
screen.getByRole('region', { name: `Carousel: ${title}` }),
76+
).toBeInTheDocument();
77+
});
78+
79+
it('does not render dots by default', () => {
80+
render(<Carousel>{mockItems}</Carousel>);
81+
expect(screen.queryByTestId('carousel-dots')).not.toBeInTheDocument();
82+
});
83+
84+
it('renders dots when withDots is true and there are multiple slides', () => {
85+
// 5 items, 4 per slide = 2 slides
86+
render(
87+
<Carousel withDots itemsDesktop={4}>
88+
{mockItems}
89+
</Carousel>,
90+
);
91+
expect(screen.getByTestId('carousel-dots')).toBeInTheDocument();
92+
});
93+
94+
it('does not render dots if there is only one slide', () => {
95+
// 5 items, 5 per slide = 1 slide
96+
render(
97+
<Carousel withDots itemsDesktop={5}>
98+
{mockItems}
99+
</Carousel>,
100+
);
101+
expect(screen.queryByTestId('carousel-dots')).not.toBeInTheDocument();
102+
});
103+
104+
it('renders navigation buttons when there are multiple slides', () => {
105+
// 5 items, 4 per slide = 2 slides
106+
render(<Carousel itemsDesktop={4}>{mockItems}</Carousel>);
107+
expect(
108+
screen.getByRole('button', { name: 'Previous' }),
109+
).toBeInTheDocument();
110+
expect(screen.getByRole('button', { name: 'Next' })).toBeInTheDocument();
111+
});
112+
113+
it('does not render navigation buttons when there is only one slide', () => {
114+
// 5 items, 5 per slide = 1 slide
115+
render(<Carousel itemsDesktop={5}>{mockItems}</Carousel>);
116+
expect(
117+
screen.queryByRole('button', { name: 'Previous' }),
118+
).not.toBeInTheDocument();
119+
expect(
120+
screen.queryByRole('button', { name: 'Next' }),
121+
).not.toBeInTheDocument();
122+
});
123+
124+
describe('responsiveness and slide calculation', () => {
125+
it('calculates number of slides based on desktop view', () => {
126+
fireResize(MOBILE_WIDTH + 1); // Desktop width
127+
const { rerender } = render(
128+
<Carousel itemsDesktop={2}>{mockItems}</Carousel>,
129+
);
130+
131+
// 5 items / 2 per slide = 3 slides. Buttons and dots should show.
132+
expect(screen.getByRole('button', { name: 'Next' })).toBeInTheDocument();
133+
134+
rerender(<Carousel itemsDesktop={5}>{mockItems}</Carousel>);
135+
136+
// 5 items / 5 per slide = 1 slide. Buttons should disappear.
137+
expect(
138+
screen.queryByRole('button', { name: 'Next' }),
139+
).not.toBeInTheDocument();
140+
});
141+
142+
it('calculates number of slides based on mobile view', () => {
143+
fireResize(MOBILE_WIDTH); // Mobile width
144+
const { rerender } = render(
145+
<Carousel itemsMobile={1}>{mockItems}</Carousel>,
146+
);
147+
148+
// 5 items / 1 per slide = 5 slides. Buttons should show.
149+
expect(screen.getByRole('button', { name: 'Next' })).toBeInTheDocument();
150+
151+
rerender(
152+
<Carousel itemsMobile={2} itemsDesktop={5}>
153+
{mockItems}
154+
</Carousel>,
155+
);
156+
157+
// 5 items / 2 per slide = 3 slides. Buttons should still show.
158+
expect(screen.getByRole('button', { name: 'Next' })).toBeInTheDocument();
159+
});
160+
161+
it('re-calculates slides when window is resized', () => {
162+
// Start with desktop view, 1 slide
163+
fireResize(MOBILE_WIDTH + 1);
164+
render(
165+
<Carousel itemsDesktop={5} itemsMobile={2}>
166+
{mockItems}
167+
</Carousel>,
168+
);
169+
expect(
170+
screen.queryByRole('button', { name: 'Next' }),
171+
).not.toBeInTheDocument();
172+
173+
// Resize to mobile, which should result in 3 slides
174+
fireResize(MOBILE_WIDTH);
175+
176+
// Buttons should now be visible
177+
expect(screen.getByRole('button', { name: 'Next' })).toBeInTheDocument();
178+
});
179+
180+
it('calculates number of slides correctly with hasMore prop', () => {
181+
// 5 items + 1 "load more" slot = 6 items. 3 per slide = 2 slides.
182+
render(
183+
<Carousel itemsDesktop={3} hasMore onLoadMore={() => {}}>
184+
{mockItems}
185+
</Carousel>,
186+
);
187+
expect(screen.getByRole('button', { name: 'Next' })).toBeInTheDocument();
188+
});
189+
});
190+
});
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import React from 'react';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import '@testing-library/jest-dom';
4+
5+
import {
6+
CarouselNextSlideButton,
7+
CarouselPreviousSlideButton,
8+
} from '../CarouselSlideButton';
9+
import { CarouselContext } from '../../context/CarouselContext';
10+
import type { CarouselContextType } from '../../types';
11+
import { initialCarouselContextValues } from '../../constants';
12+
13+
// Mock the Icon components from hds-react to isolate our button components
14+
jest.mock('hds-react', () => ({
15+
...jest.requireActual('hds-react'),
16+
IconAngleLeft: () => <div data-testid="icon-angle-left" />,
17+
IconAngleRight: () => <div data-testid="icon-angle-right" />,
18+
}));
19+
20+
// Mock useConfig to provide translations
21+
jest.mock('../../../configProvider/useConfig', () => ({
22+
useConfig: () => ({
23+
copy: {
24+
previous: 'Previous',
25+
next: 'Next',
26+
},
27+
}),
28+
}));
29+
30+
describe('CarouselSlideButton', () => {
31+
const mockHandleUpdateSlideProps = jest.fn();
32+
33+
// A helper function to render components with a mock context
34+
const renderWithContext = (
35+
ui: React.ReactElement,
36+
providerProps: Partial<CarouselContextType>,
37+
) =>
38+
render(
39+
<CarouselContext.Provider
40+
value={{
41+
...initialCarouselContextValues,
42+
// Dummy functions for context fields that require them
43+
setTransformValue: jest.fn(),
44+
setNumberOfSlides: jest.fn(),
45+
setItemsPerSlide: jest.fn(),
46+
setCurrentSlide: jest.fn(),
47+
setWidth: jest.fn(),
48+
// Real mock and provided props
49+
handleUpdateSlideProps: mockHandleUpdateSlideProps,
50+
...providerProps,
51+
}}
52+
>
53+
{ui}
54+
</CarouselContext.Provider>,
55+
);
56+
57+
beforeEach(() => {
58+
mockHandleUpdateSlideProps.mockClear();
59+
});
60+
61+
describe('CarouselPreviousSlideButton', () => {
62+
it('renders correctly and calls handler on click', () => {
63+
renderWithContext(<CarouselPreviousSlideButton />, {
64+
isReady: true,
65+
currentSlide: 1,
66+
numberOfSlides: 5,
67+
});
68+
69+
const button = screen.getByRole('button', { name: 'Previous' });
70+
expect(button).toBeInTheDocument();
71+
expect(button).not.toBeDisabled();
72+
expect(screen.getByTestId('icon-angle-left')).toBeInTheDocument();
73+
74+
fireEvent.click(button);
75+
expect(mockHandleUpdateSlideProps).toHaveBeenCalledWith(-0);
76+
});
77+
78+
it('wraps around to the last slide when on the first slide', () => {
79+
renderWithContext(<CarouselPreviousSlideButton />, {
80+
isReady: true,
81+
currentSlide: 0,
82+
numberOfSlides: 5,
83+
});
84+
85+
fireEvent.click(screen.getByRole('button', { name: 'Previous' }));
86+
expect(mockHandleUpdateSlideProps).toHaveBeenCalledWith(-4);
87+
});
88+
89+
it('is disabled when not ready', () => {
90+
renderWithContext(<CarouselPreviousSlideButton />, { isReady: false });
91+
expect(screen.getByRole('button', { name: 'Previous' })).toBeDisabled();
92+
});
93+
});
94+
95+
describe('CarouselNextSlideButton', () => {
96+
it('renders correctly and calls handler on click', () => {
97+
renderWithContext(<CarouselNextSlideButton />, {
98+
isReady: true,
99+
currentSlide: 1,
100+
numberOfSlides: 5,
101+
});
102+
103+
const button = screen.getByRole('button', { name: 'Next' });
104+
expect(button).toBeInTheDocument();
105+
expect(button).not.toBeDisabled();
106+
expect(screen.getByTestId('icon-angle-right')).toBeInTheDocument();
107+
108+
fireEvent.click(button);
109+
expect(mockHandleUpdateSlideProps).toHaveBeenCalledWith(2);
110+
});
111+
112+
it('wraps around to the first slide when on the last slide', () => {
113+
renderWithContext(<CarouselNextSlideButton />, {
114+
isReady: true,
115+
currentSlide: 4,
116+
numberOfSlides: 5,
117+
});
118+
119+
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
120+
expect(mockHandleUpdateSlideProps).toHaveBeenCalledWith(0);
121+
});
122+
});
123+
});

0 commit comments

Comments
 (0)