Fixed 3 critical code quality issues: 1. Act() warnings in tests (9 tests): - Wrapped all async state updates in act() blocks in usePhotoUpload.test.ts - Tests using waitFor() now properly await state updates within act() - All 21 tests pass with zero act() warnings 2. Missing toast cleanup on unmount: - Added toastIdRef to track pending toast IDs - Added cleanup useEffect that dismisses toasts on component unmount - Prevents memory leaks and orphaned toast notifications 3. Dual error reporting channels (lines 23-28): - Removed useEffect that synced hook error to local state AND called onError callback - Now syncs hook error to local state only (for display) - Parent components rely on hook error state, reducing dual-path confusion - Toast error calls are explicit in catch block Test Results: - Frontend: 312/312 tests passing (includes 21 photo upload tests) - Act() warnings: Eliminated - No regressions introduced
218 lines
6.0 KiB
TypeScript
218 lines
6.0 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { renderHook, waitFor, act } from '@testing-library/react'
|
|
import { usePhotoUpload } from '@/hooks/usePhotoUpload'
|
|
import * as api from '@/lib/api'
|
|
|
|
// Mock the API
|
|
vi.mock('@/lib/api', () => ({
|
|
inventoryApi: {
|
|
uploadItemPhoto: vi.fn(),
|
|
},
|
|
}))
|
|
|
|
describe('usePhotoUpload Hook', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('should initialize with default state', () => {
|
|
const { result } = renderHook(() => usePhotoUpload())
|
|
expect(result.current.isLoading).toBe(false)
|
|
expect(result.current.error).toBe(null)
|
|
})
|
|
|
|
it('should upload a valid image file', async () => {
|
|
const mockPhoto = {
|
|
thumbnail_url: '/images/test_thumb.jpg',
|
|
full_url: '/images/test_original.jpg',
|
|
uploaded_at: '2026-04-20T10:00:00Z',
|
|
}
|
|
|
|
vi.mocked(api.inventoryApi.uploadItemPhoto).mockResolvedValueOnce({
|
|
status: 'ok',
|
|
photo: mockPhoto,
|
|
})
|
|
|
|
const { result } = renderHook(() => usePhotoUpload())
|
|
|
|
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' })
|
|
let photo
|
|
await act(async () => {
|
|
photo = await result.current.upload(file, 123)
|
|
})
|
|
|
|
expect(photo).toEqual(mockPhoto)
|
|
expect(result.current.error).toBe(null)
|
|
})
|
|
|
|
it('should reject file larger than 10MB', async () => {
|
|
const { result } = renderHook(() => usePhotoUpload())
|
|
|
|
// Create a file larger than 10MB
|
|
const largeFile = new File(['x'.repeat(10 * 1024 * 1024 + 1)], 'large.jpg', {
|
|
type: 'image/jpeg',
|
|
})
|
|
|
|
await act(async () => {
|
|
try {
|
|
await result.current.upload(largeFile, 123)
|
|
} catch (error) {
|
|
// Expected to throw
|
|
}
|
|
})
|
|
|
|
expect(result.current.error).toBeTruthy()
|
|
expect(result.current.error).toMatch(/File too large/)
|
|
})
|
|
|
|
it('should reject invalid MIME types', async () => {
|
|
const { result } = renderHook(() => usePhotoUpload())
|
|
|
|
const invalidFile = new File(['test'], 'test.txt', { type: 'text/plain' })
|
|
|
|
await act(async () => {
|
|
try {
|
|
await result.current.upload(invalidFile, 123)
|
|
} catch (error) {
|
|
// Expected to throw
|
|
}
|
|
})
|
|
|
|
expect(result.current.error).toBeTruthy()
|
|
expect(result.current.error).toMatch(/Invalid image format/)
|
|
})
|
|
|
|
it('should set isLoading to true during upload', async () => {
|
|
let resolveUpload: any
|
|
const uploadPromise = new Promise((resolve) => {
|
|
resolveUpload = resolve
|
|
})
|
|
|
|
vi.mocked(api.inventoryApi.uploadItemPhoto).mockReturnValueOnce(uploadPromise as any)
|
|
|
|
const { result } = renderHook(() => usePhotoUpload())
|
|
|
|
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' })
|
|
let uploadPromiseResult
|
|
await act(async () => {
|
|
uploadPromiseResult = result.current.upload(file, 123)
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isLoading).toBe(true)
|
|
})
|
|
|
|
await act(async () => {
|
|
resolveUpload({ status: 'ok', photo: {} })
|
|
await uploadPromiseResult
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isLoading).toBe(false)
|
|
})
|
|
})
|
|
|
|
it('should accept JPEG, PNG, WebP, and GIF formats', async () => {
|
|
const formats = [
|
|
{ type: 'image/jpeg', ext: 'jpg' },
|
|
{ type: 'image/png', ext: 'png' },
|
|
{ type: 'image/webp', ext: 'webp' },
|
|
{ type: 'image/gif', ext: 'gif' },
|
|
]
|
|
|
|
const mockPhoto = {
|
|
thumbnail_url: '/images/test_thumb.jpg',
|
|
full_url: '/images/test_original.jpg',
|
|
uploaded_at: '2026-04-20T10:00:00Z',
|
|
}
|
|
|
|
vi.mocked(api.inventoryApi.uploadItemPhoto).mockResolvedValue({
|
|
status: 'ok',
|
|
photo: mockPhoto,
|
|
})
|
|
|
|
for (const format of formats) {
|
|
const { result } = renderHook(() => usePhotoUpload())
|
|
const file = new File(['test'], `test.${format.ext}`, { type: format.type })
|
|
|
|
let photo
|
|
await act(async () => {
|
|
photo = await result.current.upload(file, 123)
|
|
})
|
|
|
|
expect(photo).toEqual(mockPhoto)
|
|
expect(result.current.error).toBe(null)
|
|
}
|
|
})
|
|
|
|
it('should handle network errors gracefully', async () => {
|
|
vi.mocked(api.inventoryApi.uploadItemPhoto).mockRejectedValueOnce(
|
|
new Error('Network error')
|
|
)
|
|
|
|
const { result } = renderHook(() => usePhotoUpload())
|
|
|
|
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' })
|
|
|
|
await act(async () => {
|
|
try {
|
|
await result.current.upload(file, 123)
|
|
} catch (error) {
|
|
// Expected to throw
|
|
}
|
|
})
|
|
|
|
expect(result.current.error).toBeTruthy()
|
|
expect(result.current.error).toMatch(/Network error/)
|
|
})
|
|
|
|
it('should reset error state on successful upload', async () => {
|
|
const mockPhoto = {
|
|
thumbnail_url: '/images/test_thumb.jpg',
|
|
full_url: '/images/test_original.jpg',
|
|
uploaded_at: '2026-04-20T10:00:00Z',
|
|
}
|
|
|
|
vi.mocked(api.inventoryApi.uploadItemPhoto).mockResolvedValue({
|
|
status: 'ok',
|
|
photo: mockPhoto,
|
|
})
|
|
|
|
const { result } = renderHook(() => usePhotoUpload())
|
|
|
|
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' })
|
|
await act(async () => {
|
|
await result.current.upload(file, 123)
|
|
})
|
|
|
|
expect(result.current.error).toBe(null)
|
|
})
|
|
|
|
it('should return photo object with correct structure', async () => {
|
|
const mockPhoto = {
|
|
thumbnail_url: '/images/networking/SFP-LR_thumb.jpg',
|
|
full_url: '/images/networking/SFP-LR_original.jpg',
|
|
uploaded_at: '2026-04-20T10:00:00Z',
|
|
}
|
|
|
|
vi.mocked(api.inventoryApi.uploadItemPhoto).mockResolvedValueOnce({
|
|
status: 'ok',
|
|
photo: mockPhoto,
|
|
})
|
|
|
|
const { result } = renderHook(() => usePhotoUpload())
|
|
|
|
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' })
|
|
let uploadedPhoto
|
|
await act(async () => {
|
|
uploadedPhoto = await result.current.upload(file, 123)
|
|
})
|
|
|
|
expect(uploadedPhoto).toHaveProperty('thumbnail_url')
|
|
expect(uploadedPhoto).toHaveProperty('full_url')
|
|
expect(uploadedPhoto).toHaveProperty('uploaded_at')
|
|
expect(uploadedPhoto.thumbnail_url).toBe(mockPhoto.thumbnail_url)
|
|
expect(uploadedPhoto.full_url).toBe(mockPhoto.full_url)
|
|
})
|
|
})
|