- Vitest hook tests for useQuantityAdjustment (optimistic updates, debouncing)
- Pytest backend tests for PATCH /items/{itemId} endpoint
- Tests cover success, failure, validation, and audit logging scenarios
203 lines
5.2 KiB
TypeScript
203 lines
5.2 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { renderHook, act, waitFor } from '@testing-library/react';
|
|
import { useQuantityAdjustment } from '@/hooks/useQuantityAdjustment';
|
|
import axios from 'axios';
|
|
|
|
// Mock axios
|
|
vi.mock('axios');
|
|
const mockedAxios = axios as any;
|
|
|
|
describe('Quick Quantity Adjustment - useQuantityAdjustment Hook', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockedAxios.patch.mockClear();
|
|
});
|
|
|
|
it('initializes with correct quantity', () => {
|
|
const { result } = renderHook(() =>
|
|
useQuantityAdjustment('1', 10)
|
|
);
|
|
|
|
expect(result.current.quantity).toBe(10);
|
|
expect(result.current.isLoading).toBe(false);
|
|
expect(result.current.error).toBeNull();
|
|
});
|
|
|
|
it('updates quantity optimistically and confirms with API', async () => {
|
|
mockedAxios.patch.mockResolvedValueOnce({
|
|
data: { quantity: 15 },
|
|
});
|
|
|
|
const { result } = renderHook(() =>
|
|
useQuantityAdjustment('1', 10)
|
|
);
|
|
|
|
await act(async () => {
|
|
await result.current.adjustQuantity(15);
|
|
});
|
|
|
|
expect(result.current.quantity).toBe(15);
|
|
expect(result.current.error).toBeNull();
|
|
expect(mockedAxios.patch).toHaveBeenCalledWith(
|
|
expect.stringContaining('/items/1'),
|
|
{ quantity: 15 }
|
|
);
|
|
});
|
|
|
|
it('reverts quantity on API failure', async () => {
|
|
const error = new Error('Network error');
|
|
mockedAxios.patch.mockRejectedValueOnce(error);
|
|
|
|
const { result } = renderHook(() =>
|
|
useQuantityAdjustment('1', 10)
|
|
);
|
|
|
|
await act(async () => {
|
|
try {
|
|
await result.current.adjustQuantity(15);
|
|
} catch {
|
|
// Expected to throw
|
|
}
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.quantity).toBe(10);
|
|
expect(result.current.error).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it('validates quantity >= 0', async () => {
|
|
const { result } = renderHook(() =>
|
|
useQuantityAdjustment('1', 10)
|
|
);
|
|
|
|
await act(async () => {
|
|
await result.current.adjustQuantity(-5);
|
|
});
|
|
|
|
expect(result.current.error).toContain('non-negative');
|
|
expect(mockedAxios.patch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('validates integer quantities', async () => {
|
|
const { result } = renderHook(() =>
|
|
useQuantityAdjustment('1', 10)
|
|
);
|
|
|
|
await act(async () => {
|
|
await result.current.adjustQuantity(10.5);
|
|
});
|
|
|
|
expect(result.current.error).toContain('integer');
|
|
expect(mockedAxios.patch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('resets error on resetError call', async () => {
|
|
const error = new Error('Network error');
|
|
mockedAxios.patch.mockRejectedValueOnce(error);
|
|
|
|
const { result } = renderHook(() =>
|
|
useQuantityAdjustment('1', 10)
|
|
);
|
|
|
|
await act(async () => {
|
|
try {
|
|
await result.current.adjustQuantity(15);
|
|
} catch {
|
|
// Expected to throw
|
|
}
|
|
});
|
|
|
|
expect(result.current.error).toBeTruthy();
|
|
|
|
act(() => {
|
|
result.current.resetError();
|
|
});
|
|
|
|
expect(result.current.error).toBeNull();
|
|
});
|
|
|
|
it('debounces rapid successive calls', async () => {
|
|
mockedAxios.patch.mockResolvedValue({
|
|
data: { quantity: 15 },
|
|
});
|
|
|
|
const { result } = renderHook(() =>
|
|
useQuantityAdjustment('1', 10)
|
|
);
|
|
|
|
await act(async () => {
|
|
// Queue multiple rapid adjustments
|
|
const promises = [
|
|
result.current.adjustQuantity(12),
|
|
result.current.adjustQuantity(14),
|
|
result.current.adjustQuantity(15),
|
|
];
|
|
|
|
await Promise.all(promises);
|
|
});
|
|
|
|
// With debouncing, only the last call should fully process
|
|
// (The implementation uses 100ms debounce)
|
|
await waitFor(() => {
|
|
expect(mockedAxios.patch).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
it('handles API error responses gracefully', async () => {
|
|
mockedAxios.patch.mockRejectedValueOnce({
|
|
response: {
|
|
data: {
|
|
detail: 'Quantity exceeds available stock',
|
|
},
|
|
},
|
|
});
|
|
|
|
const { result } = renderHook(() =>
|
|
useQuantityAdjustment('1', 10)
|
|
);
|
|
|
|
await act(async () => {
|
|
try {
|
|
await result.current.adjustQuantity(999);
|
|
} catch {
|
|
// Expected
|
|
}
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.error).toBeTruthy();
|
|
expect(result.current.quantity).toBe(10); // Reverted
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Quick Quantity Adjustment - Component Integration', () => {
|
|
it('should support UI workflow: tap → +/- → commit', async () => {
|
|
// This is a placeholder for E2E/integration test that would:
|
|
// 1. Render QuantityDisplay
|
|
// 2. Click on quantity to enter edit mode
|
|
// 3. Click +/- buttons to adjust
|
|
// 4. Press Enter to commit
|
|
// 5. Verify API call with correct delta
|
|
// Implementation requires React Testing Library setup with actual component rendering
|
|
|
|
expect(true).toBe(true); // Placeholder assertion
|
|
});
|
|
|
|
it('should show loading state during API call', async () => {
|
|
// Placeholder for component loading state test
|
|
expect(true).toBe(true);
|
|
});
|
|
|
|
it('should show error toast on API failure', async () => {
|
|
// Placeholder for error handling test
|
|
expect(true).toBe(true);
|
|
});
|
|
|
|
it('should cancel edit on Escape key without API call', async () => {
|
|
// Placeholder for cancel behavior test
|
|
expect(true).toBe(true);
|
|
});
|
|
});
|