import { render, screen, fireEvent } from '@testing-library/react'; import { PhotoCarousel } from '@/components/PhotoCarousel'; import type { ListingDetailPhoto } from '@/types'; function photo(url: string, caption: string | null = null): ListingDetailPhoto { return { url, caption, type: null }; } describe('PhotoCarousel', () => { describe('B23 — single-photo and broken-image handling', () => { it('suppresses the N/M counter when there is exactly one photo', () => { render(); expect(screen.queryByText('1 / 1')).not.toBeInTheDocument(); }); it('renders the N/M counter when there are multiple photos', () => { render( , ); expect(screen.getByText('1 / 3')).toBeInTheDocument(); }); it('replaces a broken image with a placeholder tile when onError fires', () => { render( , ); const imgs = screen.getAllByRole('img'); expect(imgs).toHaveLength(2); // Simulate the second image failing to load fireEvent.error(imgs[1]); // After error, the placeholder appears and the broken img is gone expect(screen.getByText(/Photo unavailable/i)).toBeInTheDocument(); // The good image is still rendered expect(screen.getAllByRole('img')).toHaveLength(1); }); it('still renders "No photos available" when photos is empty', () => { render(); expect(screen.getByText(/No photos available/i)).toBeInTheDocument(); }); }); describe('B26 — single-photo carousel should not loop', () => { it('does not render prev/next buttons for a single-photo carousel', () => { render(); expect(screen.queryByLabelText(/Previous photo/i)).not.toBeInTheDocument(); expect(screen.queryByLabelText(/Next photo/i)).not.toBeInTheDocument(); }); it('does not render dots for a single-photo carousel', () => { const { container } = render( , ); // No "Go to photo X" buttons (dots) should be rendered when single expect(container.querySelectorAll('button[aria-label^="Go to photo"]')) .toHaveLength(0); }); it('renders prev/next + dots for a multi-photo carousel', () => { render( , ); expect(screen.getByLabelText(/Previous photo/i)).toBeInTheDocument(); expect(screen.getByLabelText(/Next photo/i)).toBeInTheDocument(); }); }); describe('B16 — keyboard navigation', () => { it('makes the multi-photo carousel root keyboard-focusable', () => { const { container } = render( , ); // The Embla overflow container is the focusable root const region = container.querySelector('[role="region"][aria-label="Property photos"]'); expect(region).not.toBeNull(); // tabIndex should be set to 0 so the carousel can receive keydown expect((region as HTMLElement).tabIndex).toBe(0); }); it('does not make a single-photo carousel keyboard-focusable', () => { const { container } = render( , ); const region = container.querySelector('[role="region"][aria-label="Property photos"]'); // Region still exists (Embla wrapping) but should not be focusable, since // there's nothing to navigate to. expect(region).not.toBeNull(); const tabIndex = (region as HTMLElement).tabIndex; // Default tabIndex on non-interactive elements is -1; we only flip to 0 // for multi-photo carousels. expect(tabIndex).toBeLessThan(0); }); }); });