- 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
278 lines
8.1 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|