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);
});
});
});