Files
tfm_ainventory/frontend/tests/integration/item-creation.test.tsx
Daniel Bedeleanu 31899be050 feat(phase2): integrate photo upload into item creation
- Create frontend/app/items/create.tsx with multi-step item creation workflow (Details → Photo Upload → Preview → Confirm)
- Create frontend/hooks/useItemCreate.ts custom hook managing form state, step navigation, and photo upload
- Add integration tests for item creation workflow with photo upload support
- Photo upload step supports manual crop UI with crop bounds submission
- ManualCropUI visible by default with toggle to use full photo
- Photo uploaded before item confirmation, ensuring photo is attached
- Works with mobile camera capture via ItemPhotoUpload component
- All 374 tests passing
2026-04-21 13:20:06 +03:00

278 lines
8.1 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, act, waitFor } from '@testing-library/react';
import { useItemCreate } from '@/hooks/useItemCreate';
import * as api from '@/lib/api';
// Mock api module
vi.mock('@/lib/api', () => ({
inventoryApi: {
createItem: vi.fn(),
uploadItemPhoto: vi.fn(),
},
}));
describe('useItemCreate Hook - Item Creation Flow Integration', () => {
beforeEach(() => {
vi.resetAllMocks();
});
it('should initialize with details step and empty form', () => {
const { result } = renderHook(() => useItemCreate());
expect(result.current.step).toBe('details');
expect(result.current.formData.name).toBe('');
expect(result.current.formData.category).toBe('');
expect(result.current.formData.item_type).toBe('');
expect(result.current.formData.quantity).toBe(1);
expect(result.current.uploadedPhoto).toBeNull();
expect(result.current.error).toBeNull();
});
it('should update form data', () => {
const { result } = renderHook(() => useItemCreate());
act(() => {
result.current.setFormData({ name: 'Test Item' });
result.current.setFormData({ category: 'Electronics' });
result.current.setFormData({ item_type: 'Component' });
});
expect(result.current.formData.name).toBe('Test Item');
expect(result.current.formData.category).toBe('Electronics');
expect(result.current.formData.item_type).toBe('Component');
});
it('should navigate between steps', async () => {
const { result } = renderHook(() => useItemCreate());
expect(result.current.step).toBe('details');
act(() => {
result.current.goToStep('photo');
});
expect(result.current.step).toBe('photo');
act(() => {
result.current.nextStep();
});
expect(result.current.step).toBe('preview');
act(() => {
result.current.prevStep();
});
expect(result.current.step).toBe('photo');
});
it('should create item and return response', async () => {
const mockCreateItem = vi.mocked(api.inventoryApi.createItem);
mockCreateItem.mockResolvedValueOnce({ id: 123, name: 'Test Item' });
const { result } = renderHook(() => useItemCreate());
act(() => {
result.current.setFormData({ name: 'Test Item' });
result.current.setFormData({ category: 'Electronics' });
result.current.setFormData({ item_type: 'Component' });
});
let createdItem;
await act(async () => {
createdItem = await result.current.submitItem(1);
});
expect(createdItem?.id).toBe(123);
expect(mockCreateItem).toHaveBeenCalledWith(1, expect.objectContaining({
name: 'Test Item',
category: 'Electronics',
item_type: 'Component',
}));
});
it('should validate required fields on submission', async () => {
const mockCreateItem = vi.mocked(api.inventoryApi.createItem);
const { result } = renderHook(() => useItemCreate());
// Try submitting without required fields (empty name)
let submitResult;
await act(async () => {
submitResult = await result.current.submitItem(1);
});
// Should return undefined and set error
expect(submitResult).toBeUndefined();
expect(result.current.error).toBe('Item name is required');
expect(mockCreateItem).not.toHaveBeenCalled();
});
it('should handle crop bounds independently', () => {
const { result } = renderHook(() => useItemCreate());
expect(result.current.cropBounds).toBeNull();
act(() => {
result.current.setCropBounds({ x: 10, y: 10, width: 100, height: 100 });
});
expect(result.current.cropBounds).toEqual({ x: 10, y: 10, width: 100, height: 100 });
act(() => {
result.current.setCropBounds(null);
});
expect(result.current.cropBounds).toBeNull();
});
it('should reset all state', async () => {
const mockCreateItem = vi.mocked(api.inventoryApi.createItem);
mockCreateItem.mockResolvedValueOnce({ id: 123 });
const { result } = renderHook(() => useItemCreate());
// Populate form and navigate
await act(async () => {
result.current.setFormData({ name: 'Test Item' });
result.current.setFormData({ category: 'Electronics' });
result.current.setFormData({ item_type: 'Component' });
result.current.goToStep('photo');
await result.current.submitItem(1);
});
// Verify populated state
expect(result.current.formData.name).toBe('Test Item');
expect(result.current.formData.category).toBe('Electronics');
// Reset
act(() => {
result.current.reset();
});
expect(result.current.step).toBe('details');
expect(result.current.formData.name).toBe('');
expect(result.current.formData.category).toBe('');
expect(result.current.uploadedPhoto).toBeNull();
expect(result.current.error).toBeNull();
expect(result.current.photoError).toBeNull();
});
it('should complete full workflow: details -> photo -> preview -> confirm', async () => {
const mockCreateItem = vi.mocked(api.inventoryApi.createItem);
const mockUploadPhoto = vi.mocked(api.inventoryApi.uploadItemPhoto);
mockCreateItem.mockResolvedValueOnce({ id: 456 });
mockUploadPhoto.mockResolvedValueOnce({
status: 'ok',
photo: {
thumbnail_url: 'http://example.com/thumb.jpg',
full_url: 'http://example.com/full.jpg',
uploaded_at: '2024-01-01T00:00:00Z',
},
});
const { result } = renderHook(() => useItemCreate());
// Step 1: Details
expect(result.current.step).toBe('details');
act(() => {
result.current.setFormData({
name: 'Workflow Item',
category: 'Electronics',
item_type: 'Component',
quantity: 5,
});
});
await act(async () => {
await result.current.submitItem(1);
});
// Step 2: Photo
act(() => {
result.current.goToStep('photo');
});
expect(result.current.step).toBe('photo');
// Step 3: Preview
act(() => {
result.current.goToStep('preview');
result.current.setCropBounds({ x: 0, y: 0, width: 100, height: 100 });
});
expect(result.current.step).toBe('preview');
expect(result.current.cropBounds).not.toBeNull();
// Step 4: Confirm
act(() => {
result.current.goToStep('confirm');
});
expect(result.current.step).toBe('confirm');
// Reset (simulating completion)
act(() => {
result.current.reset();
});
expect(result.current.step).toBe('details');
expect(result.current.formData.name).toBe('');
});
it('should call API with correct item data structure', async () => {
const mockCreateItem = vi.mocked(api.inventoryApi.createItem);
mockCreateItem.mockResolvedValueOnce({ id: 999 });
const { result } = renderHook(() => useItemCreate());
act(() => {
result.current.setFormData({
name: 'Precise Item',
category: 'Tools',
item_type: 'Power Tool',
quantity: 10,
barcode: 'BAR123',
part_number: 'PT-001',
});
});
await act(async () => {
await result.current.submitItem(5);
});
expect(mockCreateItem).toHaveBeenCalledWith(5, expect.objectContaining({
name: 'Precise Item',
category: 'Tools',
item_type: 'Power Tool',
quantity: 10,
barcode: 'BAR123',
part_number: 'PT-001',
}));
});
it('should handle API errors gracefully', async () => {
const mockCreateItem = vi.mocked(api.inventoryApi.createItem);
mockCreateItem.mockRejectedValueOnce(new Error('Network error'));
const { result } = renderHook(() => useItemCreate());
act(() => {
result.current.setFormData({ name: 'Test Item' });
result.current.setFormData({ category: 'Electronics' });
result.current.setFormData({ item_type: 'Component' });
});
let submitResult;
await act(async () => {
submitResult = await result.current.submitItem(1);
});
// Should return undefined on error
expect(submitResult).toBeUndefined();
// Error should be set (may need to wait for state update)
await waitFor(() => {
expect(result.current.error).toBeTruthy();
});
});
});