+ ));
+ const propsWithManyChildren = {
+ ...getDefaultProps(),
+ children: manyChildren,
+ positions: Array.from({ length: 8 }, (_, i) => -i * 250),
+ maxActiveItem: 6, // 8 - 2 = 6
+ };
+
+ rerender();
+
+ // All items should be rendered
+ expect(screen.getByText('Item 1')).toBeInTheDocument();
+ expect(screen.getByText('Item 8')).toBeInTheDocument();
+ });
+
+ it('sets up drag constraints correctly', () => {
+ renderWithChakra();
+
+ // Component should render without errors
+ expect(screen.getByText('Item 1')).toBeInTheDocument();
+ });
+
+ it('updates position when activeItem changes', () => {
+ // Animation calls are not tested due to mocking
+ // Only tests that the component responds to activeItem changes
+ const { rerender } = renderWithChakra();
+
+ // Change activeItem and re-render
+ const updatedProps = {
+ ...getDefaultProps(),
+ activeItem: 1, // Move to second position
+ };
+
+ rerender();
+
+ // Component should still render correctly with new activeItem
+ expect(screen.getByText('Item 1')).toBeInTheDocument();
+ });
+
+ it('sets track active state based on click target', () => {
+ const { container } = renderWithChakra();
+
+ // Get the track element (should be the container)
+ const trackElement = container.firstChild as HTMLElement;
+
+ // Simulate clicking inside the track
+ fireEvent.mouseDown(trackElement);
+
+ // setTrackIsActive should be called
+ expect(mockSetTrackIsActive).toHaveBeenCalled();
+ });
+
+ it('handles clicks outside the track element', () => {
+ renderWithChakra();
+
+ // Create a click event on a different element
+ const outsideElement = document.createElement('div');
+ document.body.appendChild(outsideElement);
+
+ // Simulate clicking outside the track
+ fireEvent.mouseDown(outsideElement);
+
+ // setTrackIsActive should be called with false (clicked outside)
+ expect(mockSetTrackIsActive).toHaveBeenCalledWith(false);
+
+ // Clean up
+ document.body.removeChild(outsideElement);
+ });
+
+ it('handles drag start correctly', () => {
+ const props = {
+ ...getDefaultProps(),
+ activeItem: 1, // Start at position 1
+ };
+
+ renderWithChakra();
+
+ // Find the draggable element using the test ID
+ const trackElement = screen.getByTestId('draggable-track');
+
+ // Simulate drag start
+ fireEvent.mouseDown(trackElement);
+
+ // The component should store the starting position internally
+ expect(trackElement).toBeInTheDocument();
+ });
+
+ it('calculates correct target position on drag end', () => {
+ renderWithChakra();
+
+ // Find the draggable element using the test ID added
+ const trackElement = screen.getByTestId('draggable-track');
+
+ // First simulate drag start with mouseDown
+ fireEvent.mouseDown(trackElement);
+
+ // Then simulate drag end with mouseUp - this will trigger the mock drag end handler
+ fireEvent.mouseUp(trackElement);
+
+ // The component should calculate the closest position and set activeItem
+ expect(mockSetActiveItem).toHaveBeenCalled();
+ });
+
+ it('handles keyboard navigation with arrow keys', () => {
+ const props = {
+ ...getDefaultProps(),
+ trackIsActive: true, // Track must be active for keyboard nav to work
+ activeItem: 1, // Start at position 1
+ };
+
+ renderWithChakra();
+
+ // Simulate pressing the right arrow key
+ fireEvent.keyDown(document, { key: 'ArrowRight' });
+
+ // Should call setActiveItem with a function that increments position
+ expect(mockSetActiveItem).toHaveBeenCalledWith(expect.any(Function));
+
+ // Test the function that was passed to setActiveItem
+ const updateFunction = mockSetActiveItem.mock.calls[0][0];
+ expect(updateFunction(1)).toBe(2); // Math.min(1 + 2, 2) = 2
+ });
+
+ it('handles up and down arrow keys same as left and right', () => {
+ const props = {
+ ...getDefaultProps(),
+ trackIsActive: true,
+ activeItem: 1,
+ };
+
+ renderWithChakra();
+
+ // Test ArrowUp (should work like ArrowRight)
+ fireEvent.keyDown(document, { key: 'ArrowUp' });
+ expect(mockSetActiveItem).toHaveBeenCalledWith(expect.any(Function));
+
+ // Reset and test ArrowDown (should work like ArrowLeft)
+ mockSetActiveItem.mockClear();
+ fireEvent.keyDown(document, { key: 'ArrowDown' });
+ expect(mockSetActiveItem).toHaveBeenCalledWith(expect.any(Function));
+ });
+
+ it('ignores keyboard events when track is inactive', () => {
+ const props = {
+ ...getDefaultProps(),
+ trackIsActive: false, // Track is not active
+ };
+
+ renderWithChakra();
+
+ // Try keyboard navigation
+ fireEvent.keyDown(document, { key: 'ArrowRight' });
+ fireEvent.keyDown(document, { key: 'ArrowLeft' });
+
+ // Should not respond to keyboard when inactive
+ expect(mockSetActiveItem).not.toHaveBeenCalled();
+ });
+
+ it('responds differently based on trackIsActive state', () => {
+ // Test with active track
+ const { unmount } = renderWithChakra(
+ ,
+ );
+
+ // Keyboard events should work when track is active
+ fireEvent.keyDown(document, { key: 'ArrowRight' });
+ expect(mockSetActiveItem).toHaveBeenCalled();
+
+ // Unmount the first component to clean up its event listeners
+ unmount();
+
+ // Reset and test with inactive track
+ mockSetActiveItem.mockClear();
+
+ renderWithChakra(
+ ,
+ );
+
+ // Keyboard events should be ignored when track is inactive
+ fireEvent.keyDown(document, { key: 'ArrowRight' });
+ expect(mockSetActiveItem).not.toHaveBeenCalled();
+ });
+
+ it('prevents default behavior for relevant keyboard events', () => {
+ const propsWithActiveTrack = {
+ ...getDefaultProps(),
+ trackIsActive: true,
+ activeItem: 1, // In middle, so both directions are valid
+ };
+
+ renderWithChakra();
+
+ // Create events with preventDefault mock
+ const rightArrowEvent = new KeyboardEvent('keydown', {
+ key: 'ArrowRight',
+ });
+ const preventDefaultSpy = jest.spyOn(rightArrowEvent, 'preventDefault');
+
+ fireEvent(document, rightArrowEvent);
+
+ // Should prevent default browser behavior for arrow keys
+ expect(preventDefaultSpy).toHaveBeenCalled();
+
+ preventDefaultSpy.mockRestore();
+ });
+
+ it('respects boundaries when navigating with keyboard', () => {
+ const props = {
+ ...getDefaultProps(),
+ trackIsActive: true,
+ activeItem: 2, // At the end (maxActiveItem)
+ };
+
+ renderWithChakra();
+
+ // Try to go right when already at the end
+ fireEvent.keyDown(document, { key: 'ArrowRight' });
+
+ // As activeItem (2) >= maxActiveItem (2), it shouldn't call setActiveItem
+ expect(mockSetActiveItem).not.toHaveBeenCalled();
+
+ // Reset and test left arrow
+ mockSetActiveItem.mockClear();
+
+ // Test left arrow (should work)
+ fireEvent.keyDown(document, { key: 'ArrowLeft' });
+ expect(mockSetActiveItem).toHaveBeenCalledWith(expect.any(Function));
+
+ const updateFunction = mockSetActiveItem.mock.calls[0][0];
+ expect(updateFunction(2)).toBe(0); // Math.max(2 - 2, 0) = 0
+ });
+
+ it('prevents navigation beyond boundaries with keyboard', () => {
+ const propsAtStart = {
+ ...getDefaultProps(),
+ trackIsActive: true,
+ activeItem: 0,
+ };
+
+ renderWithChakra();
+
+ // Try to go left when already at the beginning
+ fireEvent.keyDown(document, { key: 'ArrowLeft' });
+
+ // Should not call setActiveItem because activeItem (0) <= 0
+ expect(mockSetActiveItem).not.toHaveBeenCalled();
+ });
+
+ it('handles keyboard navigation when constraint exceeds remaining items', () => {
+ const propsWithLargeConstraint = {
+ ...getDefaultProps(),
+ trackIsActive: true,
+ activeItem: 1,
+ constraint: 5, // Larger than number of remaining items
+ maxActiveItem: 2,
+ };
+
+ renderWithChakra();
+
+ // Press right arrow
+ fireEvent.keyDown(document, { key: 'ArrowRight' });
+
+ expect(mockSetActiveItem).toHaveBeenCalledWith(expect.any(Function));
+
+ // Test the update function
+ const updateFunction = mockSetActiveItem.mock.calls[0][0];
+ // Should be limited by maxActiveItem: Math.min(1 + 5, 2) = 2
+ expect(updateFunction(1)).toBe(2);
+ });
+
+ it('handles horizontal wheel events for navigation', () => {
+ renderWithChakra();
+
+ // Create a horizontal wheel event (like trackpad horizontal scroll)
+ const wheelEvent = new WheelEvent('wheel', {
+ deltaX: 100,
+ deltaY: 10,
+ bubbles: true,
+ });
+
+ // Dispatch the wheel event on the document
+ fireEvent(document, wheelEvent);
+
+ // Should update activeItem based on scroll direction
+ expect(mockSetActiveItem).toHaveBeenCalled();
+ });
+
+ it('ignores wheel events that are primarily vertical', () => {
+ renderWithChakra();
+
+ // Create a mostly vertical wheel event (normal page scrolling)
+ const wheelEvent = new WheelEvent('wheel', {
+ deltaX: 10,
+ deltaY: 100,
+ bubbles: true,
+ });
+
+ fireEvent(document, wheelEvent);
+
+ // Should ignore this event since |deltaY| > |deltaX|
+ expect(mockSetActiveItem).not.toHaveBeenCalled();
+ });
+
+ it('prevents default behavior for horizontal wheel events', () => {
+ renderWithChakra();
+
+ // Create wheel event with preventDefault spy
+ const wheelEvent = new WheelEvent('wheel', {
+ deltaX: 100,
+ deltaY: 10,
+ });
+ const preventDefaultSpy = jest.spyOn(wheelEvent, 'preventDefault');
+ const stopPropagationSpy = jest.spyOn(wheelEvent, 'stopPropagation');
+
+ fireEvent(document, wheelEvent);
+
+ // Should prevent default and stop propagation for horizontal scrolling
+ expect(preventDefaultSpy).toHaveBeenCalled();
+ expect(stopPropagationSpy).toHaveBeenCalled();
+
+ preventDefaultSpy.mockRestore();
+ stopPropagationSpy.mockRestore();
+ });
+
+ it('works with custom drag multiplier', () => {
+ const propsWithCustomMultiplier = {
+ ...getDefaultProps(),
+ multiplier: 0.5,
+ };
+
+ renderWithChakra();
+
+ // Component should render normally with custom multiplier
+ expect(screen.getByText('Item 1')).toBeInTheDocument();
+ });
+
+ it('handles empty positions array gracefully', () => {
+ const propsWithEmptyPositions = {
+ ...getDefaultProps(),
+ positions: [], // No positions
+ children: [], // No children
+ maxActiveItem: 0,
+ };
+
+ renderWithChakra();
+
+ // Should render without crashing
+ expect(mockSetTrackIsActive).toBeDefined();
+ });
+
+ it('removes event listeners on unmount', () => {
+ // Spy on document event listener methods to verify cleanup
+ const addEventListenerSpy = jest.spyOn(document, 'addEventListener');
+ const removeEventListenerSpy = jest.spyOn(document, 'removeEventListener');
+
+ const { unmount } = renderWithChakra();
+
+ // Verify that event listeners were added
+ expect(addEventListenerSpy).toHaveBeenCalledWith(
+ 'keydown',
+ expect.any(Function),
+ );
+ expect(addEventListenerSpy).toHaveBeenCalledWith(
+ 'mousedown',
+ expect.any(Function),
+ );
+ expect(addEventListenerSpy).toHaveBeenCalledWith(
+ 'wheel',
+ expect.any(Function),
+ { passive: false },
+ );
+
+ unmount();
+
+ // Verify that event listeners were removed
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
+ 'keydown',
+ expect.any(Function),
+ );
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
+ 'mousedown',
+ expect.any(Function),
+ );
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
+ 'wheel',
+ expect.any(Function),
+ );
+
+ // Clean up spies
+ addEventListenerSpy.mockRestore();
+ removeEventListenerSpy.mockRestore();
+ });
+});
diff --git a/src/components/carousel/__tests__/constants.test.ts b/src/components/carousel/__tests__/constants.test.ts
new file mode 100644
index 00000000..59f6b920
--- /dev/null
+++ b/src/components/carousel/__tests__/constants.test.ts
@@ -0,0 +1,38 @@
+import { TRANSITION_PROPS, PROGRESS_BAR_THRESHOLDS } from '../constants';
+
+describe('Carousel Constants', () => {
+ it('has correct transition properties for spring animation', () => {
+ expect(TRANSITION_PROPS.spring).toEqual({
+ type: 'spring',
+ stiffness: 200,
+ damping: 60,
+ mass: 3,
+ });
+ });
+
+ it('has correct transition properties for ease animation', () => {
+ expect(TRANSITION_PROPS.ease).toEqual({
+ type: 'ease',
+ ease: 'easeInOut',
+ duration: 0.4,
+ });
+ });
+
+ it('has correct progress bar threshold values', () => {
+ expect(PROGRESS_BAR_THRESHOLDS).toEqual({
+ SINGLE_ITEM: 10,
+ TWO_ITEMS: 18,
+ THREE_OR_MORE: 25,
+ MIN_WIDTH: 300,
+ });
+ });
+
+ it('has logical progression in threshold values', () => {
+ expect(PROGRESS_BAR_THRESHOLDS.SINGLE_ITEM).toBeLessThan(
+ PROGRESS_BAR_THRESHOLDS.TWO_ITEMS,
+ );
+ expect(PROGRESS_BAR_THRESHOLDS.TWO_ITEMS).toBeLessThan(
+ PROGRESS_BAR_THRESHOLDS.THREE_OR_MORE,
+ );
+ });
+});
diff --git a/src/components/carousel/__tests__/index.test.tsx b/src/components/carousel/__tests__/index.test.tsx
new file mode 100644
index 00000000..6b4ffaa1
--- /dev/null
+++ b/src/components/carousel/__tests__/index.test.tsx
@@ -0,0 +1,538 @@
+import React from 'react';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { ChakraProvider } from '@chakra-ui/react';
+import { Carousel } from '../index';
+
+// Mock the useResizeObserver hook (it's used to detect container width)
+jest.mock('usehooks-ts', () => ({
+ useResizeObserver: jest.fn(() => ({
+ width: 800,
+ height: 400,
+ })),
+}));
+
+// Mock Chakra UI's useMediaQuery (employed in useCarouselState)
+jest.mock('@chakra-ui/react', () => ({
+ ...jest.requireActual('@chakra-ui/react'),
+ useMediaQuery: jest.fn(() => [false]),
+}));
+
+// Mock the theme import with non-overlapping breakpoints
+jest.mock('src/theme', () => ({
+ theme: {
+ breakpoints: {
+ base: '0px',
+ sm: '480px',
+ md: '768px',
+ lg: '992px',
+ xl: '1280px',
+ },
+ },
+}));
+
+// Mock framer-motion to simplify animation testing
+jest.mock('framer-motion', () => ({
+ motion: (Component: any) => {
+ const MotionComponent = React.forwardRef(
+ ({ children, ...props }, ref) => {
+ const {
+ animate,
+ initial,
+ exit,
+ transition,
+ variants,
+ drag,
+ dragConstraints,
+ dragElastic,
+ dragMomentum,
+ dragTransition,
+ whileHover,
+ whileTap,
+ whileFocus,
+ whileDrag,
+ whileInView,
+ onAnimationStart,
+ onAnimationComplete,
+ onHoverStart,
+ onHoverEnd,
+ onTap,
+ onTapStart,
+ onTapCancel,
+ onDrag,
+ onDragStart,
+ onDragEnd,
+ onDirectionLock,
+ onViewportEnter,
+ onViewportLeave,
+ style,
+ ...domProps
+ } = props;
+
+ return (
+
+ {children}
+
+ );
+ },
+ );
+ MotionComponent.displayName = `Motion${
+ Component.displayName || Component.name || 'Component'
+ }`;
+ return MotionComponent;
+ },
+ useAnimation: () => ({
+ start: jest.fn(),
+ }),
+ useMotionValue: (initialValue: number) => ({
+ get: jest.fn(() => initialValue),
+ set: jest.fn(),
+ }),
+}));
+
+const renderWithChakra = (ui: React.ReactElement) => {
+ return render({ui});
+};
+
+// Mock media queries for different screen sizes with non-overlapping ranges
+const mockMediaQueries = (screenSize: 'mobile' | 'tablet' | 'desktop') => {
+ const { useMediaQuery } = require('@chakra-ui/react');
+
+ switch (screenSize) {
+ case 'mobile':
+ // Mobile: 0px to 767px (below md breakpoint) - constraint: 1
+ (useMediaQuery as jest.Mock)
+ .mockReturnValueOnce([true]) // isBetweenBaseAndMd: (max-width: 767px)
+ .mockReturnValueOnce([false]) // isBetweenMdAndXl: (min-width: 768px) and (max-width: 1279px)
+ .mockReturnValueOnce([false]); // isGreaterThanXL: (min-width: 1280px)
+ break;
+
+ case 'tablet':
+ // Tablet: 768px to 1279px - constraint: 2
+ (useMediaQuery as jest.Mock)
+ .mockReturnValueOnce([false])
+ .mockReturnValueOnce([true])
+ .mockReturnValueOnce([false]);
+ break;
+
+ case 'desktop':
+ // Desktop: 1280px and above - constraint: 3
+ (useMediaQuery as jest.Mock)
+ .mockReturnValueOnce([false])
+ .mockReturnValueOnce([false])
+ .mockReturnValueOnce([true]);
+ break;
+ }
+};
+
+describe('Carousel', () => {
+ // Sample carousel items for testing
+ const mockChildren = [
+
Carousel Item 1
,
+
Carousel Item 2
,
+
Carousel Item 3
,
+
Carousel Item 4
,
+
Carousel Item 5
,
+ ];
+
+ const defaultProps = {
+ colorScheme: 'primary',
+ gap: 32,
+ isLoading: false,
+ } as const;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ // Reset useMediaQuery to default state
+ const { useMediaQuery } = require('@chakra-ui/react');
+ (useMediaQuery as jest.Mock).mockReturnValue([false]);
+ });
+
+ it('renders carousel with all children and controls', () => {
+ mockMediaQueries('tablet');
+
+ renderWithChakra({mockChildren});
+
+ // All carousel items should be rendered
+ expect(screen.getByText('Carousel Item 1')).toBeInTheDocument();
+ expect(screen.getByText('Carousel Item 2')).toBeInTheDocument();
+ expect(screen.getByText('Carousel Item 3')).toBeInTheDocument();
+ expect(screen.getByText('Carousel Item 4')).toBeInTheDocument();
+ expect(screen.getByText('Carousel Item 5')).toBeInTheDocument();
+
+ // Navigation controls should be rendered
+ expect(screen.getByLabelText('previous carousel item')).toBeInTheDocument();
+ expect(screen.getByLabelText('next carousel item')).toBeInTheDocument();
+
+ // Should have pagination dots (5 items, constraint = 2 => 3 dots total)
+ expect(
+ screen.getByLabelText('carousel indicator 1 of 3 (current)'),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByLabelText('carousel indicator 2 of 3'),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByLabelText('carousel indicator 3 of 3'),
+ ).toBeInTheDocument();
+ });
+
+ it('works with minimal props using default values', () => {
+ mockMediaQueries('mobile');
+
+ // Only provide required children, no other props
+ renderWithChakra({mockChildren});
+
+ expect(screen.getByText('Carousel Item 1')).toBeInTheDocument();
+ expect(screen.getByLabelText('previous carousel item')).toBeInTheDocument();
+ });
+
+ it('has correct DOM structure and CSS classes', () => {
+ mockMediaQueries('tablet');
+
+ const { container } = renderWithChakra(
+ {mockChildren},
+ );
+
+ // Should have the padded-carousel class
+ const paddedCarousel = container.querySelector('.padded-carousel');
+ expect(paddedCarousel).toBeInTheDocument();
+
+ // Should have items with the 'item' class
+ const items = container.querySelectorAll('.item');
+ expect(items).toHaveLength(5);
+ });
+
+ it('renders complex children content correctly', () => {
+ mockMediaQueries('tablet');
+
+ // Create complex carousel items
+ const complexChildren = [
+
+
Card 1 Title
+
Card 1 description
+
+
,
+
+
Card 2 Title
+
Card 2 description
+
+
,
+
+
Test Image Placeholder
+ Image caption
+
,
+ ];
+
+ renderWithChakra({complexChildren});
+
+ // All complex content should be rendered
+ expect(screen.getByText('Card 1 Title')).toBeInTheDocument();
+ expect(screen.getByText('Card 1 description')).toBeInTheDocument();
+ expect(screen.getByText('Action 1')).toBeInTheDocument();
+ expect(screen.getByText('Card 2 Title')).toBeInTheDocument();
+ expect(screen.getByText('Test Image Placeholder')).toBeInTheDocument();
+ expect(screen.getByText('Image caption')).toBeInTheDocument();
+ });
+
+ it('allows navigation between items using controls', async () => {
+ const user = userEvent.setup();
+ mockMediaQueries('tablet'); // Tablet (constraint: 2)
+
+ renderWithChakra({mockChildren});
+
+ // Previous button should be disabled (at start)
+ const prevButton = screen.getByLabelText('previous carousel item');
+ const nextButton = screen.getByLabelText('next carousel item');
+
+ expect(prevButton).toBeDisabled();
+ expect(nextButton).not.toBeDisabled();
+
+ // Click next button to advance
+ await user.click(nextButton);
+
+ // After clicking next, the previous button shouldn't be disabled
+ await waitFor(() => {
+ expect(prevButton).not.toBeDisabled();
+ });
+ });
+
+ it('allows navigation using pagination dots', async () => {
+ const user = userEvent.setup();
+ mockMediaQueries('tablet'); // Tablet: (5 items, constraint = 2 => 3 dots total)
+
+ renderWithChakra({mockChildren});
+
+ // Initially, first dot should be active (current)
+ expect(
+ screen.getByLabelText('carousel indicator 1 of 3 (current)'),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByLabelText('carousel indicator 2 of 3'),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByLabelText('carousel indicator 3 of 3'),
+ ).toBeInTheDocument();
+
+ // Previous button should be disabled at start
+ const prevButton = screen.getByLabelText('previous carousel item');
+ const nextButton = screen.getByLabelText('next carousel item');
+ expect(prevButton).toBeDisabled();
+ expect(nextButton).not.toBeDisabled();
+
+ // Click on the second dot to navigate
+ const secondDot = screen.getByLabelText('carousel indicator 2 of 3');
+ await user.click(secondDot);
+
+ // Wait for state updates and verify navigation occurred
+ await waitFor(() => {
+ // Second dot should now be active
+ expect(
+ screen.getByLabelText('carousel indicator 2 of 3 (current)'),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByLabelText('carousel indicator 1 of 3'),
+ ).toBeInTheDocument();
+
+ // Navigation buttons should both be enabled
+ expect(prevButton).not.toBeDisabled();
+ expect(nextButton).not.toBeDisabled();
+ });
+
+ // Click on the third (last) dot
+ const thirdDot = screen.getByLabelText('carousel indicator 3 of 3');
+ await user.click(thirdDot);
+
+ await waitFor(() => {
+ // Third dot should now be active
+ expect(
+ screen.getByLabelText('carousel indicator 3 of 3 (current)'),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByLabelText('carousel indicator 2 of 3'),
+ ).toBeInTheDocument();
+
+ // Next button should be disabled
+ expect(prevButton).not.toBeDisabled();
+ expect(nextButton).toBeDisabled();
+ });
+
+ // Navigate back to first dot
+ const firstDot = screen.getByLabelText('carousel indicator 1 of 3');
+ await user.click(firstDot);
+
+ await waitFor(() => {
+ // First dot should be active again
+ expect(
+ screen.getByLabelText('carousel indicator 1 of 3 (current)'),
+ ).toBeInTheDocument();
+
+ // Should be back at start, so previous button disabled
+ expect(prevButton).toBeDisabled();
+ expect(nextButton).not.toBeDisabled();
+ });
+ });
+
+ it('shows controls when items exceed constraint', () => {
+ mockMediaQueries('mobile'); // Mobile layout (constraint: 1)
+
+ renderWithChakra(
+
+ {[
Item 1
,
Item 2
]}
+ ,
+ );
+
+ // Should show controls because 2 items > 1 constraint
+ expect(screen.getByLabelText('previous carousel item')).toBeInTheDocument();
+ expect(screen.getByLabelText('next carousel item')).toBeInTheDocument();
+ });
+
+ it('hides controls when all items fit in viewport', () => {
+ mockMediaQueries('desktop'); // Desktop layout (constraint: 3)
+
+ // Create 3 items for desktop layout
+ const exactFitChildren = [
+
Item 1
,
+
Item 2
,
+
Item 3
, // items = constraint = 3
+ ];
+
+ renderWithChakra({exactFitChildren});
+
+ // Should render all items
+ expect(screen.getByText('Item 1')).toBeInTheDocument();
+ expect(screen.getByText('Item 2')).toBeInTheDocument();
+ expect(screen.getByText('Item 3')).toBeInTheDocument();
+
+ // Should not show navigation controls
+ expect(
+ screen.queryByLabelText('previous carousel item'),
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByLabelText('next carousel item'),
+ ).not.toBeInTheDocument();
+ });
+
+ it('shows progress bar when there are many items', () => {
+ mockMediaQueries('mobile');
+
+ // Create many items to trigger progress bar
+ const manyChildren = Array.from({ length: 15 }, (_, i) => (
+
Item {i + 1}
+ ));
+
+ renderWithChakra({manyChildren});
+
+ // Should show progress bar instead of dots - get the main progress bar
+ const progressBars = screen.getAllByRole('progressbar');
+ expect(progressBars.length).toBeGreaterThan(0);
+
+ // Should not show individual dots when progress bar is shown
+ expect(
+ screen.queryByLabelText(/carousel indicator \d+ of/),
+ ).not.toBeInTheDocument();
+ });
+
+ it('integrates responsive behavior correctly across breakpoints with boundary testing', () => {
+ const threeChildren = [
+