Files
tfm_ainventory/frontend/tests/hooks/useItemCreate.test.ts

434 lines
14 KiB
TypeScript

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);
});
});
});