164 lines
5.4 KiB
TypeScript
164 lines
5.4 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { render, screen, waitFor } from '@testing-library/react'
|
|
import ItemPhotoUpload from '@/components/ItemPhotoUpload'
|
|
import * as api from '@/lib/api'
|
|
|
|
// Mock react-hot-toast
|
|
vi.mock('react-hot-toast', () => ({
|
|
toast: {
|
|
error: vi.fn(),
|
|
success: vi.fn(),
|
|
loading: vi.fn(),
|
|
},
|
|
}))
|
|
|
|
// Mock the API
|
|
vi.mock('@/lib/api', () => ({
|
|
inventoryApi: {
|
|
uploadItemPhoto: vi.fn(),
|
|
},
|
|
}))
|
|
|
|
describe('ItemPhotoUpload Component', () => {
|
|
const mockOnUploadSuccess = vi.fn()
|
|
const mockOnError = vi.fn()
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('should render with upload and camera buttons', () => {
|
|
render(
|
|
<ItemPhotoUpload itemId={123} onUploadSuccess={mockOnUploadSuccess} onError={mockOnError} />
|
|
)
|
|
|
|
expect(screen.getByRole('button', { name: /upload/i })).toBeInTheDocument()
|
|
expect(screen.getByRole('button', { name: /camera/i })).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render hidden file input for uploads', () => {
|
|
const { container } = render(
|
|
<ItemPhotoUpload itemId={123} onUploadSuccess={mockOnUploadSuccess} onError={mockOnError} />
|
|
)
|
|
|
|
const fileInputs = container.querySelectorAll('input[type="file"]')
|
|
expect(fileInputs.length).toBeGreaterThanOrEqual(1)
|
|
})
|
|
|
|
it('should render file input with accept image/* attribute', () => {
|
|
const { container } = render(
|
|
<ItemPhotoUpload itemId={123} onUploadSuccess={mockOnUploadSuccess} onError={mockOnError} />
|
|
)
|
|
|
|
const fileInputs = Array.from(container.querySelectorAll('input[type="file"]'))
|
|
const uploadInput = fileInputs.find(
|
|
(el) => (el as HTMLInputElement).accept.includes('image')
|
|
) as HTMLInputElement
|
|
|
|
expect(uploadInput).toBeTruthy()
|
|
expect(uploadInput.accept).toContain('image')
|
|
})
|
|
|
|
it('should render camera input with capture environment attribute', () => {
|
|
const { container } = render(
|
|
<ItemPhotoUpload itemId={123} onUploadSuccess={mockOnUploadSuccess} onError={mockOnError} />
|
|
)
|
|
|
|
const fileInputs = Array.from(container.querySelectorAll('input[type="file"]'))
|
|
const cameraInput = fileInputs.find(
|
|
(el) => (el as HTMLInputElement).getAttribute('capture') !== null
|
|
) as HTMLInputElement
|
|
|
|
expect(cameraInput).toBeTruthy()
|
|
expect(cameraInput.getAttribute('capture')).toBe('environment')
|
|
})
|
|
|
|
it('should accept itemId prop', () => {
|
|
const { rerender } = render(
|
|
<ItemPhotoUpload itemId={123} onUploadSuccess={mockOnUploadSuccess} onError={mockOnError} />
|
|
)
|
|
|
|
// Component should render without errors with different itemId
|
|
rerender(
|
|
<ItemPhotoUpload itemId={456} onUploadSuccess={mockOnUploadSuccess} onError={mockOnError} />
|
|
)
|
|
|
|
const buttons = screen.queryAllByRole('button')
|
|
expect(buttons.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('should have onUploadSuccess callback prop', () => {
|
|
const customCallback = vi.fn()
|
|
render(
|
|
<ItemPhotoUpload itemId={123} onUploadSuccess={customCallback} onError={mockOnError} />
|
|
)
|
|
|
|
const buttons = screen.queryAllByRole('button')
|
|
expect(buttons.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('should have onError callback prop', () => {
|
|
const customErrorCallback = vi.fn()
|
|
render(
|
|
<ItemPhotoUpload itemId={123} onUploadSuccess={mockOnUploadSuccess} onError={customErrorCallback} />
|
|
)
|
|
|
|
const buttons = screen.queryAllByRole('button')
|
|
expect(buttons.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('should render buttons with proper styling', () => {
|
|
const { container } = render(
|
|
<ItemPhotoUpload itemId={123} onUploadSuccess={mockOnUploadSuccess} onError={mockOnError} />
|
|
)
|
|
|
|
const uploadButton = screen.getByRole('button', { name: /upload/i })
|
|
const cameraButton = screen.getByRole('button', { name: /camera/i })
|
|
|
|
// Check for Tailwind classes (basic check)
|
|
expect(uploadButton.className).toContain('rounded')
|
|
expect(cameraButton.className).toContain('rounded')
|
|
})
|
|
|
|
it('should render responsive layout with gap', () => {
|
|
const { container } = render(
|
|
<ItemPhotoUpload itemId={123} onUploadSuccess={mockOnUploadSuccess} onError={mockOnError} />
|
|
)
|
|
|
|
const wrapper = container.querySelector('.flex.flex-col.gap-3')
|
|
expect(wrapper).toBeInTheDocument()
|
|
})
|
|
|
|
it('should display loading state message when isLoading is true', async () => {
|
|
// Since component manages loading state internally, we check if the
|
|
// component can display loading messages (integration tested via hook)
|
|
const { container } = render(
|
|
<ItemPhotoUpload itemId={123} onUploadSuccess={mockOnUploadSuccess} onError={mockOnError} />
|
|
)
|
|
|
|
// Component renders with buttons
|
|
const buttons = screen.queryAllByRole('button')
|
|
expect(buttons.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('should be accessible with aria labels', () => {
|
|
const { container } = render(
|
|
<ItemPhotoUpload itemId={123} onUploadSuccess={mockOnUploadSuccess} onError={mockOnError} />
|
|
)
|
|
|
|
const fileInputs = Array.from(container.querySelectorAll('input[type="file"]'))
|
|
fileInputs.forEach((input) => {
|
|
expect((input as HTMLInputElement).getAttribute('aria-label')).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
it('should work on mobile and desktop without errors', () => {
|
|
// Component should render without throwing
|
|
const { container } = render(
|
|
<ItemPhotoUpload itemId={123} onUploadSuccess={mockOnUploadSuccess} onError={mockOnError} />
|
|
)
|
|
|
|
expect(container.firstChild).toBeTruthy()
|
|
})
|
|
})
|