import { renderHook, act } from '@testing-library/react'; import * as toast from 'react-hot-toast'; import { useItemCreate } from '@/hooks/useItemCreate'; import { inventoryApi } from '@/lib/api'; vi.mock('react-hot-toast'); vi.mock('@/lib/api'); describe('useItemCreate - Auto-Upload Photo After Item Creation', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('submitItem with auto-upload', () => { it('should auto-upload photo after item creation if image_processing provided', async () => { const { result } = renderHook(() => useItemCreate()); const mockBlob = new Blob(['test'], { type: 'image/jpeg' }); const mockImageProcessing = { crop_bounds: { x: 10, y: 20, width: 100, height: 100 }, rotation_degrees: 0, confidence: 0.95, }; const mockCreatedItem = { id: 123, name: 'Test Item' }; (inventoryApi.createItem as any).mockResolvedValue(mockCreatedItem); (inventoryApi.uploadItemPhoto as any).mockResolvedValue({ status: 'ok', photo: { thumbnail_url: 'https://example.com/thumb.jpg', full_url: 'https://example.com/full.jpg', uploaded_at: '2026-04-21T10:00:00Z', }, }); // Set form data with image data act(() => { result.current.setFormData({ name: 'Test Item', category: 'Electronics', item_type: 'Widget', quantity: 5, extractedImageBlob: mockBlob, imageProcessing: mockImageProcessing, }); }); // Submit item const createdItem = await act(async () => { return result.current.submitItem(1); }); // Verify item was created (without image fields) const createCall = (inventoryApi.createItem as any).mock.calls[0]; expect(createCall[0]).toBe(1); expect(createCall[1]).not.toHaveProperty('extractedImageBlob'); expect(createCall[1]).not.toHaveProperty('imageProcessing'); expect(createCall[1].name).toBe('Test Item'); expect(createCall[1].category).toBe('Electronics'); expect(createCall[1].item_type).toBe('Widget'); expect(createCall[1].quantity).toBe(5); // Verify photo was uploaded with correct parameters expect(inventoryApi.uploadItemPhoto).toHaveBeenCalled(); const uploadCall = (inventoryApi.uploadItemPhoto as any).mock.calls[0]; expect(uploadCall[0]).toBe(123); // itemId expect(uploadCall[1]).toBeInstanceOf(FormData); // formData // Check FormData contents (FormData.get returns File, not Blob) const formDataUpload = uploadCall[1]; const uploadedFile = formDataUpload.get('file'); expect(uploadedFile).toBeInstanceOf(Blob); expect(uploadedFile?.type).toBe('image/jpeg'); expect(formDataUpload.get('crop_bounds')).toBe( JSON.stringify(mockImageProcessing.crop_bounds) ); // Verify item was returned expect(createdItem).toEqual(mockCreatedItem); }); it('should skip photo upload if image_processing missing', async () => { const { result } = renderHook(() => useItemCreate()); const mockBlob = new Blob(['test'], { type: 'image/jpeg' }); const mockCreatedItem = { id: 123, name: 'Test Item' }; (inventoryApi.createItem as any).mockResolvedValue(mockCreatedItem); // Set form data with ONLY image blob (no imageProcessing) act(() => { result.current.setFormData({ name: 'Test Item', category: 'Electronics', item_type: 'Widget', quantity: 5, extractedImageBlob: mockBlob, // imageProcessing NOT provided }); }); // Submit item const createdItem = await act(async () => { return result.current.submitItem(1); }); // Verify photo upload was NOT called expect(inventoryApi.uploadItemPhoto).not.toHaveBeenCalled(); // Verify item was created expect(createdItem).toEqual(mockCreatedItem); }); it('should skip photo upload if extractedImageBlob missing', async () => { const { result } = renderHook(() => useItemCreate()); const mockImageProcessing = { crop_bounds: { x: 10, y: 20, width: 100, height: 100 }, rotation_degrees: 0, confidence: 0.95, }; const mockCreatedItem = { id: 123, name: 'Test Item' }; (inventoryApi.createItem as any).mockResolvedValue(mockCreatedItem); // Set form data with ONLY imageProcessing (no blob) act(() => { result.current.setFormData({ name: 'Test Item', category: 'Electronics', item_type: 'Widget', quantity: 5, // extractedImageBlob NOT provided imageProcessing: mockImageProcessing, }); }); // Submit item const createdItem = await act(async () => { return result.current.submitItem(1); }); // Verify photo upload was NOT called expect(inventoryApi.uploadItemPhoto).not.toHaveBeenCalled(); // Verify item was created expect(createdItem).toEqual(mockCreatedItem); }); it('should handle photo upload failure gracefully (item already created)', async () => { const { result } = renderHook(() => useItemCreate()); const mockBlob = new Blob(['test'], { type: 'image/jpeg' }); const mockImageProcessing = { crop_bounds: { x: 10, y: 20, width: 100, height: 100 }, rotation_degrees: 0, confidence: 0.95, }; const mockCreatedItem = { id: 123, name: 'Test Item' }; (inventoryApi.createItem as any).mockResolvedValue(mockCreatedItem); (inventoryApi.uploadItemPhoto as any).mockRejectedValue( new Error('Network error') ); // Set form data with image data act(() => { result.current.setFormData({ name: 'Test Item', category: 'Electronics', item_type: 'Widget', quantity: 5, extractedImageBlob: mockBlob, imageProcessing: mockImageProcessing, }); }); // Submit item await act(async () => { return result.current.submitItem(1); }); // Verify item was created despite photo upload failure expect(inventoryApi.createItem).toHaveBeenCalled(); expect(inventoryApi.uploadItemPhoto).toHaveBeenCalled(); // Photo upload error doesn't prevent item creation // The key behavior is that both API calls happen: // 1. createItem succeeds // 2. uploadItemPhoto fails gracefully (doesn't throw) }); it('should not auto-upload if neither blob nor imageProcessing provided', async () => { const { result } = renderHook(() => useItemCreate()); const mockCreatedItem = { id: 123, name: 'Test Item' }; (inventoryApi.createItem as any).mockResolvedValue(mockCreatedItem); // Set form data WITHOUT image data act(() => { result.current.setFormData({ name: 'Test Item', category: 'Electronics', item_type: 'Widget', quantity: 5, }); }); // Submit item const createdItem = await act(async () => { return result.current.submitItem(1); }); // Verify photo upload was NOT called expect(inventoryApi.uploadItemPhoto).not.toHaveBeenCalled(); // Verify item was created expect(createdItem).toEqual(mockCreatedItem); }); it('should extract image fields from formData and exclude from item creation', async () => { const { result } = renderHook(() => useItemCreate()); const mockBlob = new Blob(['test'], { type: 'image/jpeg' }); const mockImageProcessing = { crop_bounds: { x: 10, y: 20, width: 100, height: 100 }, rotation_degrees: 0, confidence: 0.95, }; const mockCreatedItem = { id: 123, name: 'Test Item' }; (inventoryApi.createItem as any).mockResolvedValue(mockCreatedItem); (inventoryApi.uploadItemPhoto as any).mockResolvedValue({ status: 'ok', }); // Set form data with image data act(() => { result.current.setFormData({ name: 'Test Item', category: 'Electronics', item_type: 'Widget', quantity: 5, barcode: '123456', extractedImageBlob: mockBlob, imageProcessing: mockImageProcessing, }); }); // Submit item await act(async () => { return result.current.submitItem(1); }); // Verify createItem was called WITHOUT image fields const createCall = (inventoryApi.createItem as any).mock.calls[0]; expect(createCall[0]).toBe(1); expect(createCall[1]).not.toHaveProperty('extractedImageBlob'); expect(createCall[1]).not.toHaveProperty('imageProcessing'); expect(createCall[1].name).toBe('Test Item'); expect(createCall[1].category).toBe('Electronics'); expect(createCall[1].item_type).toBe('Widget'); expect(createCall[1].quantity).toBe(5); expect(createCall[1].barcode).toBe('123456'); }); it('should pass crop_bounds to uploadItemPhoto if present', async () => { const { result } = renderHook(() => useItemCreate()); const mockBlob = new Blob(['test'], { type: 'image/jpeg' }); const mockImageProcessing = { crop_bounds: { x: 50, y: 100, width: 200, height: 200 }, rotation_degrees: 90, confidence: 0.87, }; const mockCreatedItem = { id: 456, name: 'Test Item' }; (inventoryApi.createItem as any).mockResolvedValue(mockCreatedItem); (inventoryApi.uploadItemPhoto as any).mockResolvedValue({ status: 'ok', }); // Set form data with image data act(() => { result.current.setFormData({ name: 'Test Item', category: 'Electronics', item_type: 'Widget', quantity: 5, extractedImageBlob: mockBlob, imageProcessing: mockImageProcessing, }); }); // Submit item await act(async () => { return result.current.submitItem(1); }); // Verify crop_bounds were passed const uploadCall = (inventoryApi.uploadItemPhoto as any).mock.calls[0]; const formDataUpload = uploadCall[1]; expect(formDataUpload.get('crop_bounds')).toBe( JSON.stringify(mockImageProcessing.crop_bounds) ); }); it('should skip crop_bounds if not in imageProcessing', async () => { const { result } = renderHook(() => useItemCreate()); const mockBlob = new Blob(['test'], { type: 'image/jpeg' }); const mockImageProcessing = { rotation_degrees: 0, confidence: 0.95, // crop_bounds NOT provided }; const mockCreatedItem = { id: 789, name: 'Test Item' }; (inventoryApi.createItem as any).mockResolvedValue(mockCreatedItem); (inventoryApi.uploadItemPhoto as any).mockResolvedValue({ status: 'ok', }); // Set form data with image data act(() => { result.current.setFormData({ name: 'Test Item', category: 'Electronics', item_type: 'Widget', quantity: 5, extractedImageBlob: mockBlob, imageProcessing: mockImageProcessing, }); }); // Submit item await act(async () => { return result.current.submitItem(1); }); // Verify crop_bounds were NOT passed const uploadCall = (inventoryApi.uploadItemPhoto as any).mock.calls[0]; const formDataUpload = uploadCall[1]; expect(formDataUpload.get('crop_bounds')).toBeNull(); }); }); describe('existing submitItem functionality (non-photo flow)', () => { it('should validate required fields on item creation', async () => { const { result } = renderHook(() => useItemCreate()); // Set form data with missing category act(() => { result.current.setFormData({ name: 'Test Item', category: '', item_type: 'Widget', quantity: 5, }); }); // Submit item const createdItem = await act(async () => { return result.current.submitItem(1); }); // Verify validation failed expect(inventoryApi.createItem).not.toHaveBeenCalled(); expect(result.current.error).toBe('Category is required'); expect(createdItem).toBeUndefined(); }); it('should handle item creation API errors', async () => { const { result } = renderHook(() => useItemCreate()); (inventoryApi.createItem as any).mockRejectedValue( new Error('Server error') ); // Set form data act(() => { result.current.setFormData({ name: 'Test Item', category: 'Electronics', item_type: 'Widget', quantity: 5, }); }); // Submit item const createdItem = await act(async () => { return result.current.submitItem(1); }); // Verify error was set expect(result.current.error).toBe('Server error'); expect(createdItem).toBeUndefined(); }); it('should set itemId when item creation succeeds', async () => { const { result } = renderHook(() => useItemCreate()); const mockCreatedItem = { id: 999, name: 'Test Item' }; (inventoryApi.createItem as any).mockResolvedValue(mockCreatedItem); // Set form data act(() => { result.current.setFormData({ name: 'Test Item', category: 'Electronics', item_type: 'Widget', quantity: 5, }); }); // Submit item const createdItem = await act(async () => { return result.current.submitItem(1); }); // Verify itemId is now set (used for photo uploads) expect(createdItem).toEqual(mockCreatedItem); }); }); });