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