2026-01-07 09:31:02 +00:00
|
|
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
|
|
|
import { describe, it, expect, vi } from 'vitest';
|
|
|
|
|
import { ImageViewerModal } from './ImageViewerModal';
|
|
|
|
|
|
|
|
|
|
describe('ImageViewerModal Component', () => {
|
|
|
|
|
const mockOnClose = vi.fn();
|
|
|
|
|
const mockOnNext = vi.fn();
|
|
|
|
|
const mockOnPrev = vi.fn();
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders image', () => {
|
|
|
|
|
render(
|
|
|
|
|
<ImageViewerModal
|
|
|
|
|
src="test.jpg"
|
|
|
|
|
alt="Test image"
|
|
|
|
|
onClose={mockOnClose}
|
2026-01-13 18:47:57 +00:00
|
|
|
/>,
|
2026-01-07 09:31:02 +00:00
|
|
|
);
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
const image = screen.getByAltText('Test image');
|
|
|
|
|
expect(image).toBeInTheDocument();
|
|
|
|
|
expect(image).toHaveAttribute('src', 'test.jpg');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('calls onClose when close button is clicked', () => {
|
|
|
|
|
render(
|
2026-01-13 18:47:57 +00:00
|
|
|
<ImageViewerModal src="test.jpg" alt="Test" onClose={mockOnClose} />,
|
2026-01-07 09:31:02 +00:00
|
|
|
);
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
const closeButton = screen.getByTitle('Close');
|
|
|
|
|
fireEvent.click(closeButton);
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
expect(mockOnClose).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
feat(ui): badge polish, DnD feedback, typography system, image lightbox
Badge component:
- Dismissible variant with X button (onDismiss prop)
- Pulse animation variant (pulse prop)
- Enhanced dot-only mode for standalone colored circles
Drag-and-drop visual feedback:
- PlaylistTrackListSortableItem: opacity + shadow + ring on drag, border insertion line
- QueueView: dragOverIndex tracking, bg-primary/5 drop zone highlight
- PlaylistDetailView: same DnD feedback pattern
Typography standardization:
- 9 utility classes: text-display, text-heading-1..4, text-body-lg, text-body, text-caption, text-label
- Applied to 8 page headings (Dashboard, Settings, Library, Search, etc.)
- DESIGN_TOKENS.md updated with typography reference
Image lightbox:
- Keyboard navigation: ArrowLeft/Right for gallery, Escape to close
- Image counter pill: "2 / 5" with backdrop-blur
- Zoom toggle: click to zoom in/out with scale transition
- Loading skeleton: pulse placeholder while image loads
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 22:52:33 +00:00
|
|
|
it('calls onClose when Escape key is pressed', () => {
|
|
|
|
|
render(
|
|
|
|
|
<ImageViewerModal src="test.jpg" alt="Test" onClose={mockOnClose} />,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
fireEvent.keyDown(document, { key: 'Escape' });
|
|
|
|
|
|
|
|
|
|
expect(mockOnClose).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('calls onNext when ArrowRight key is pressed', () => {
|
|
|
|
|
render(
|
|
|
|
|
<ImageViewerModal
|
|
|
|
|
src="test.jpg"
|
|
|
|
|
alt="Test"
|
|
|
|
|
onClose={mockOnClose}
|
|
|
|
|
onNext={mockOnNext}
|
|
|
|
|
hasNext={true}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
fireEvent.keyDown(document, { key: 'ArrowRight' });
|
|
|
|
|
|
|
|
|
|
expect(mockOnNext).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('calls onPrev when ArrowLeft key is pressed', () => {
|
|
|
|
|
render(
|
|
|
|
|
<ImageViewerModal
|
|
|
|
|
src="test.jpg"
|
|
|
|
|
alt="Test"
|
|
|
|
|
onClose={mockOnClose}
|
|
|
|
|
onPrev={mockOnPrev}
|
|
|
|
|
hasPrev={true}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
fireEvent.keyDown(document, { key: 'ArrowLeft' });
|
|
|
|
|
|
|
|
|
|
expect(mockOnPrev).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('does not call onNext on ArrowRight when hasNext is false', () => {
|
|
|
|
|
render(
|
|
|
|
|
<ImageViewerModal
|
|
|
|
|
src="test.jpg"
|
|
|
|
|
alt="Test"
|
|
|
|
|
onClose={mockOnClose}
|
|
|
|
|
onNext={mockOnNext}
|
|
|
|
|
hasNext={false}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
fireEvent.keyDown(document, { key: 'ArrowRight' });
|
|
|
|
|
|
|
|
|
|
expect(mockOnNext).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
it('shows next button when hasNext is true', () => {
|
|
|
|
|
render(
|
|
|
|
|
<ImageViewerModal
|
|
|
|
|
src="test.jpg"
|
|
|
|
|
alt="Test"
|
|
|
|
|
onClose={mockOnClose}
|
|
|
|
|
onNext={mockOnNext}
|
|
|
|
|
hasNext={true}
|
2026-01-13 18:47:57 +00:00
|
|
|
/>,
|
2026-01-07 09:31:02 +00:00
|
|
|
);
|
2026-01-13 18:47:57 +00:00
|
|
|
|
feat(ui): badge polish, DnD feedback, typography system, image lightbox
Badge component:
- Dismissible variant with X button (onDismiss prop)
- Pulse animation variant (pulse prop)
- Enhanced dot-only mode for standalone colored circles
Drag-and-drop visual feedback:
- PlaylistTrackListSortableItem: opacity + shadow + ring on drag, border insertion line
- QueueView: dragOverIndex tracking, bg-primary/5 drop zone highlight
- PlaylistDetailView: same DnD feedback pattern
Typography standardization:
- 9 utility classes: text-display, text-heading-1..4, text-body-lg, text-body, text-caption, text-label
- Applied to 8 page headings (Dashboard, Settings, Library, Search, etc.)
- DESIGN_TOKENS.md updated with typography reference
Image lightbox:
- Keyboard navigation: ArrowLeft/Right for gallery, Escape to close
- Image counter pill: "2 / 5" with backdrop-blur
- Zoom toggle: click to zoom in/out with scale transition
- Loading skeleton: pulse placeholder while image loads
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 22:52:33 +00:00
|
|
|
const nextButton = screen.getByLabelText('Next image');
|
2026-01-07 09:31:02 +00:00
|
|
|
expect(nextButton).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('calls onNext when next button is clicked', () => {
|
|
|
|
|
render(
|
|
|
|
|
<ImageViewerModal
|
|
|
|
|
src="test.jpg"
|
|
|
|
|
alt="Test"
|
|
|
|
|
onClose={mockOnClose}
|
|
|
|
|
onNext={mockOnNext}
|
|
|
|
|
hasNext={true}
|
2026-01-13 18:47:57 +00:00
|
|
|
/>,
|
2026-01-07 09:31:02 +00:00
|
|
|
);
|
2026-01-13 18:47:57 +00:00
|
|
|
|
feat(ui): badge polish, DnD feedback, typography system, image lightbox
Badge component:
- Dismissible variant with X button (onDismiss prop)
- Pulse animation variant (pulse prop)
- Enhanced dot-only mode for standalone colored circles
Drag-and-drop visual feedback:
- PlaylistTrackListSortableItem: opacity + shadow + ring on drag, border insertion line
- QueueView: dragOverIndex tracking, bg-primary/5 drop zone highlight
- PlaylistDetailView: same DnD feedback pattern
Typography standardization:
- 9 utility classes: text-display, text-heading-1..4, text-body-lg, text-body, text-caption, text-label
- Applied to 8 page headings (Dashboard, Settings, Library, Search, etc.)
- DESIGN_TOKENS.md updated with typography reference
Image lightbox:
- Keyboard navigation: ArrowLeft/Right for gallery, Escape to close
- Image counter pill: "2 / 5" with backdrop-blur
- Zoom toggle: click to zoom in/out with scale transition
- Loading skeleton: pulse placeholder while image loads
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 22:52:33 +00:00
|
|
|
const nextButton = screen.getByLabelText('Next image');
|
|
|
|
|
fireEvent.click(nextButton);
|
|
|
|
|
expect(mockOnNext).toHaveBeenCalled();
|
2026-01-07 09:31:02 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows prev button when hasPrev is true', () => {
|
|
|
|
|
render(
|
|
|
|
|
<ImageViewerModal
|
|
|
|
|
src="test.jpg"
|
|
|
|
|
alt="Test"
|
|
|
|
|
onClose={mockOnClose}
|
|
|
|
|
onPrev={mockOnPrev}
|
|
|
|
|
hasPrev={true}
|
2026-01-13 18:47:57 +00:00
|
|
|
/>,
|
2026-01-07 09:31:02 +00:00
|
|
|
);
|
2026-01-13 18:47:57 +00:00
|
|
|
|
feat(ui): badge polish, DnD feedback, typography system, image lightbox
Badge component:
- Dismissible variant with X button (onDismiss prop)
- Pulse animation variant (pulse prop)
- Enhanced dot-only mode for standalone colored circles
Drag-and-drop visual feedback:
- PlaylistTrackListSortableItem: opacity + shadow + ring on drag, border insertion line
- QueueView: dragOverIndex tracking, bg-primary/5 drop zone highlight
- PlaylistDetailView: same DnD feedback pattern
Typography standardization:
- 9 utility classes: text-display, text-heading-1..4, text-body-lg, text-body, text-caption, text-label
- Applied to 8 page headings (Dashboard, Settings, Library, Search, etc.)
- DESIGN_TOKENS.md updated with typography reference
Image lightbox:
- Keyboard navigation: ArrowLeft/Right for gallery, Escape to close
- Image counter pill: "2 / 5" with backdrop-blur
- Zoom toggle: click to zoom in/out with scale transition
- Loading skeleton: pulse placeholder while image loads
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 22:52:33 +00:00
|
|
|
const prevButton = screen.getByLabelText('Previous image');
|
|
|
|
|
expect(prevButton).toBeInTheDocument();
|
2026-01-07 09:31:02 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('uses default alt text when alt is not provided', () => {
|
2026-01-13 18:47:57 +00:00
|
|
|
render(<ImageViewerModal src="test.jpg" onClose={mockOnClose} />);
|
|
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
expect(screen.getByText('Image Preview')).toBeInTheDocument();
|
|
|
|
|
});
|
feat(ui): badge polish, DnD feedback, typography system, image lightbox
Badge component:
- Dismissible variant with X button (onDismiss prop)
- Pulse animation variant (pulse prop)
- Enhanced dot-only mode for standalone colored circles
Drag-and-drop visual feedback:
- PlaylistTrackListSortableItem: opacity + shadow + ring on drag, border insertion line
- QueueView: dragOverIndex tracking, bg-primary/5 drop zone highlight
- PlaylistDetailView: same DnD feedback pattern
Typography standardization:
- 9 utility classes: text-display, text-heading-1..4, text-body-lg, text-body, text-caption, text-label
- Applied to 8 page headings (Dashboard, Settings, Library, Search, etc.)
- DESIGN_TOKENS.md updated with typography reference
Image lightbox:
- Keyboard navigation: ArrowLeft/Right for gallery, Escape to close
- Image counter pill: "2 / 5" with backdrop-blur
- Zoom toggle: click to zoom in/out with scale transition
- Loading skeleton: pulse placeholder while image loads
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 22:52:33 +00:00
|
|
|
|
|
|
|
|
it('shows image counter when currentIndex and totalImages are provided', () => {
|
|
|
|
|
render(
|
|
|
|
|
<ImageViewerModal
|
|
|
|
|
src="test.jpg"
|
|
|
|
|
alt="Test"
|
|
|
|
|
onClose={mockOnClose}
|
|
|
|
|
currentIndex={2}
|
|
|
|
|
totalImages={5}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText('2 / 5')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('does not show image counter for single images', () => {
|
|
|
|
|
render(
|
|
|
|
|
<ImageViewerModal
|
|
|
|
|
src="test.jpg"
|
|
|
|
|
alt="Test"
|
|
|
|
|
onClose={mockOnClose}
|
|
|
|
|
currentIndex={1}
|
|
|
|
|
totalImages={1}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(screen.queryByText('1 / 1')).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('toggles zoom on image click', () => {
|
|
|
|
|
render(
|
|
|
|
|
<ImageViewerModal src="test.jpg" alt="Test" onClose={mockOnClose} />,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const image = screen.getByAltText('Test');
|
|
|
|
|
|
|
|
|
|
// Initially in fit mode (cursor-zoom-in)
|
|
|
|
|
expect(image.className).toContain('cursor-zoom-in');
|
|
|
|
|
|
|
|
|
|
// Click to zoom
|
|
|
|
|
fireEvent.click(image);
|
|
|
|
|
expect(image.className).toContain('cursor-zoom-out');
|
|
|
|
|
|
|
|
|
|
// Click to unzoom
|
|
|
|
|
fireEvent.click(image);
|
|
|
|
|
expect(image.className).toContain('cursor-zoom-in');
|
|
|
|
|
});
|
2026-01-07 09:31:02 +00:00
|
|
|
});
|