|
| 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 | +}); |
0 commit comments