Files
tfm_ainventory/frontend/tests/components/InventoryTable.photo.test.tsx
Daniel Bedeleanu 3df15cf68f feat(phase2): add photo display to inventory card with modal viewer
- Create PhotoModal component for full-res photo viewing
- Add photo thumbnail (200px square) to inventory item card
- Implement photo modal trigger on thumbnail click
- Add fallback text when no photo available
- Modal closeable via X button, click outside, or Escape key
- Image scales responsively without stretching
- Add comprehensive test coverage (30+ tests)
- All 427 tests passing, build successful
- TypeScript strict mode compliant
2026-04-21 14:53:27 +03:00

500 lines
14 KiB
TypeScript

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import InventoryTable from '@/components/InventoryTable';
import { Item } from '@/lib/db';
// Mock ItemDetailModal and PhotoModal
vi.mock('@/components/ItemDetailModal', () => ({
default: ({ item, onClose }: any) => (
<div data-testid="item-detail-modal">
<p>{item.name}</p>
<button onClick={onClose}>Close Detail</button>
</div>
),
}));
vi.mock('@/components/PhotoModal', () => ({
default: ({ photoUrl, title, onClose }: any) => (
<div data-testid="photo-modal">
<p>{title}</p>
<img src={photoUrl} alt={title} />
<button onClick={onClose}>Close Photo</button>
</div>
),
}));
describe('InventoryTable - Photo Display', () => {
const mockOnExpandCategory = vi.fn();
const mockOnItemClick = vi.fn();
const createMockItem = (overrides?: Partial<Item>): Item => ({
id: 1,
barcode: 'TEST-001',
name: 'Test Item',
category: 'Electronics',
quantity: 10,
min_quantity: 5,
...overrides,
});
beforeEach(() => {
mockOnExpandCategory.mockClear();
mockOnItemClick.mockClear();
});
describe('Photo Thumbnail Display', () => {
it('should render photo thumbnail when image_url exists', () => {
const items = [
createMockItem({
id: 1,
name: 'Item with Photo',
image_url: 'https://example.com/photo.jpg',
}),
];
render(
<InventoryTable
items={items}
categories={['Electronics']}
expandedCategory="Electronics"
onExpandCategory={mockOnExpandCategory}
onItemClick={mockOnItemClick}
/>
);
const thumbnail = screen.getByAltText('Item with Photo');
expect(thumbnail).toBeInTheDocument();
expect(thumbnail).toHaveAttribute('src', 'https://example.com/photo.jpg');
});
it('should render fallback icon when no image_url', () => {
const items = [createMockItem({ id: 1, name: 'Item without Photo' })];
render(
<InventoryTable
items={items}
categories={['Electronics']}
expandedCategory="Electronics"
onExpandCategory={mockOnExpandCategory}
onItemClick={mockOnItemClick}
/>
);
// Should not have image
expect(screen.queryByAltText('Item without Photo')).not.toBeInTheDocument();
// Should have "No photo" text
expect(screen.getByText('No photo')).toBeInTheDocument();
});
it('should apply border styling to thumbnail', () => {
const items = [
createMockItem({
id: 1,
image_url: 'https://example.com/photo.jpg',
}),
];
const { container } = render(
<InventoryTable
items={items}
categories={['Electronics']}
expandedCategory="Electronics"
onExpandCategory={mockOnExpandCategory}
onItemClick={mockOnItemClick}
/>
);
// Find thumbnail container (parent of img)
const thumbnail = screen.getByAltText('Test Item');
expect(thumbnail.parentElement).toHaveClass(
'border-2',
'border-slate-300'
);
});
it('should have proper dimensions for thumbnail', () => {
const items = [
createMockItem({
id: 1,
image_url: 'https://example.com/photo.jpg',
}),
];
const { container } = render(
<InventoryTable
items={items}
categories={['Electronics']}
expandedCategory="Electronics"
onExpandCategory={mockOnExpandCategory}
onItemClick={mockOnItemClick}
/>
);
const thumbnail = screen.getByAltText('Test Item');
expect(thumbnail.parentElement).toHaveClass('w-12', 'h-12');
});
it('should show "Tap photo for details" hint when photo exists', () => {
const items = [
createMockItem({
id: 1,
image_url: 'https://example.com/photo.jpg',
}),
];
render(
<InventoryTable
items={items}
categories={['Electronics']}
expandedCategory="Electronics"
onExpandCategory={mockOnExpandCategory}
onItemClick={mockOnItemClick}
/>
);
expect(screen.getByText('Tap photo for details')).toBeInTheDocument();
});
it('should use lazy loading for thumbnail images', () => {
const items = [
createMockItem({
id: 1,
image_url: 'https://example.com/photo.jpg',
}),
];
render(
<InventoryTable
items={items}
categories={['Electronics']}
expandedCategory="Electronics"
onExpandCategory={mockOnExpandCategory}
onItemClick={mockOnItemClick}
/>
);
const thumbnail = screen.getByAltText('Test Item');
expect(thumbnail).toHaveAttribute('loading', 'lazy');
});
});
describe('Photo Modal Interaction', () => {
it('should open PhotoModal when clicking thumbnail', () => {
const items = [
createMockItem({
id: 1,
name: 'Item with Photo',
image_url: 'https://example.com/photo.jpg',
}),
];
render(
<InventoryTable
items={items}
categories={['Electronics']}
expandedCategory="Electronics"
onExpandCategory={mockOnExpandCategory}
onItemClick={mockOnItemClick}
/>
);
const thumbnail = screen.getByAltText('Item with Photo');
fireEvent.click(thumbnail);
// PhotoModal should be rendered with correct props
expect(screen.getByTestId('photo-modal')).toBeInTheDocument();
const photoHeaders = screen.getAllByText('Item with Photo');
expect(photoHeaders.length).toBeGreaterThan(0);
});
it('should pass correct photo URL to PhotoModal', () => {
const photoUrl = 'https://example.com/test-photo.jpg';
const items = [
createMockItem({
id: 1,
name: 'Test Item',
image_url: photoUrl,
}),
];
render(
<InventoryTable
items={items}
categories={['Electronics']}
expandedCategory="Electronics"
onExpandCategory={mockOnExpandCategory}
onItemClick={mockOnItemClick}
/>
);
const thumbnail = screen.getByAltText('Test Item');
fireEvent.click(thumbnail);
// Find photo modal and verify URL
const modal = screen.getByTestId('photo-modal');
const photoImages = modal.querySelectorAll('img');
expect(photoImages.length).toBeGreaterThan(0);
expect(photoImages[0]).toHaveAttribute('src', photoUrl);
});
it('should close PhotoModal when close button clicked', () => {
const items = [
createMockItem({
id: 1,
image_url: 'https://example.com/photo.jpg',
}),
];
render(
<InventoryTable
items={items}
categories={['Electronics']}
expandedCategory="Electronics"
onExpandCategory={mockOnExpandCategory}
onItemClick={mockOnItemClick}
/>
);
const thumbnail = screen.getByAltText('Test Item');
fireEvent.click(thumbnail);
expect(screen.getByTestId('photo-modal')).toBeInTheDocument();
const closeButton = screen.getByText('Close Photo');
fireEvent.click(closeButton);
expect(screen.queryByTestId('photo-modal')).not.toBeInTheDocument();
});
it('should not show PhotoModal if image_url is undefined', () => {
const items = [createMockItem({ id: 1 })];
render(
<InventoryTable
items={items}
categories={['Electronics']}
expandedCategory="Electronics"
onExpandCategory={mockOnExpandCategory}
onItemClick={mockOnItemClick}
/>
);
expect(screen.queryByTestId('photo-modal')).not.toBeInTheDocument();
});
});
describe('Item Click Behavior', () => {
it('should open ItemDetailModal when clicking item name/specs', () => {
const items = [
createMockItem({
id: 1,
name: 'Item Name',
specs: 'Test specs',
image_url: 'https://example.com/photo.jpg',
}),
];
render(
<InventoryTable
items={items}
categories={['Electronics']}
expandedCategory="Electronics"
onExpandCategory={mockOnExpandCategory}
onItemClick={mockOnItemClick}
/>
);
// Click on the item name (not the thumbnail)
const itemName = screen.getByText('Item Name');
fireEvent.click(itemName);
expect(screen.getByTestId('item-detail-modal')).toBeInTheDocument();
});
it('should NOT trigger item detail when clicking thumbnail', () => {
const items = [
createMockItem({
id: 1,
image_url: 'https://example.com/photo.jpg',
}),
];
render(
<InventoryTable
items={items}
categories={['Electronics']}
expandedCategory="Electronics"
onExpandCategory={mockOnExpandCategory}
onItemClick={mockOnItemClick}
/>
);
const thumbnail = screen.getByAltText('Test Item');
fireEvent.click(thumbnail);
// Only PhotoModal should be shown, not ItemDetailModal
expect(screen.queryByTestId('item-detail-modal')).not.toBeInTheDocument();
expect(screen.getByTestId('photo-modal')).toBeInTheDocument();
});
it('should call onItemClick when item is selected', () => {
const items = [
createMockItem({
id: 1,
name: 'Test Item',
}),
];
render(
<InventoryTable
items={items}
categories={['Electronics']}
expandedCategory="Electronics"
onExpandCategory={mockOnExpandCategory}
onItemClick={mockOnItemClick}
/>
);
const itemName = screen.getByText('Test Item');
fireEvent.click(itemName);
expect(mockOnItemClick).toHaveBeenCalled();
});
});
describe('Multiple Items', () => {
it('should display multiple items with mixed photo states', () => {
const items = [
createMockItem({
id: 1,
name: 'Item A',
image_url: 'https://example.com/photo-a.jpg',
}),
createMockItem({
id: 2,
name: 'Item B',
image_url: undefined,
}),
createMockItem({
id: 3,
name: 'Item C',
image_url: 'https://example.com/photo-c.jpg',
}),
];
render(
<InventoryTable
items={items}
categories={['Electronics']}
expandedCategory="Electronics"
onExpandCategory={mockOnExpandCategory}
onItemClick={mockOnItemClick}
/>
);
// Check Item A has photo
expect(screen.getByAltText('Item A')).toBeInTheDocument();
// Check Item B has no photo text
const itemBText = screen.getAllByText('No photo');
expect(itemBText.length).toBeGreaterThanOrEqual(1);
// Check Item C has photo
expect(screen.getByAltText('Item C')).toBeInTheDocument();
});
it('should open correct PhotoModal for each item', () => {
const items = [
createMockItem({
id: 1,
name: 'Item A',
image_url: 'https://example.com/photo-a.jpg',
}),
createMockItem({
id: 2,
name: 'Item C',
image_url: 'https://example.com/photo-c.jpg',
}),
];
render(
<InventoryTable
items={items}
categories={['Electronics']}
expandedCategory="Electronics"
onExpandCategory={mockOnExpandCategory}
onItemClick={mockOnItemClick}
/>
);
// Click Item A thumbnail
const thumbnailA = screen.getByAltText('Item A');
fireEvent.click(thumbnailA);
let photoModal = screen.getByTestId('photo-modal');
expect(photoModal).toHaveTextContent('Item A');
// Close and open Item C
let closeButton = screen.getByText('Close Photo');
fireEvent.click(closeButton);
const thumbnailC = screen.getByAltText('Item C');
fireEvent.click(thumbnailC);
photoModal = screen.getByTestId('photo-modal');
expect(photoModal).toHaveTextContent('Item C');
});
});
describe('Styling and Interaction States', () => {
it('should apply hover styling to thumbnail', () => {
const items = [
createMockItem({
id: 1,
image_url: 'https://example.com/photo.jpg',
}),
];
render(
<InventoryTable
items={items}
categories={['Electronics']}
expandedCategory="Electronics"
onExpandCategory={mockOnExpandCategory}
onItemClick={mockOnItemClick}
/>
);
const thumbnail = screen.getByAltText('Test Item');
expect(thumbnail.parentElement).toHaveClass(
'hover:border-primary',
'transition-colors'
);
});
it('should apply active state to thumbnail', () => {
const items = [
createMockItem({
id: 1,
image_url: 'https://example.com/photo.jpg',
}),
];
render(
<InventoryTable
items={items}
categories={['Electronics']}
expandedCategory="Electronics"
onExpandCategory={mockOnExpandCategory}
onItemClick={mockOnItemClick}
/>
);
const thumbnail = screen.getByAltText('Test Item');
expect(thumbnail.parentElement).toHaveClass('active:scale-95');
});
});
});