- 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
500 lines
14 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|