fix(phase2): resolve code quality issues in ItemPhotoUpload (act warnings, toast cleanup, dual error handling)
Fixed 3 critical code quality issues: 1. Act() warnings in tests (9 tests): - Wrapped all async state updates in act() blocks in usePhotoUpload.test.ts - Tests using waitFor() now properly await state updates within act() - All 21 tests pass with zero act() warnings 2. Missing toast cleanup on unmount: - Added toastIdRef to track pending toast IDs - Added cleanup useEffect that dismisses toasts on component unmount - Prevents memory leaks and orphaned toast notifications 3. Dual error reporting channels (lines 23-28): - Removed useEffect that synced hook error to local state AND called onError callback - Now syncs hook error to local state only (for display) - Parent components rely on hook error state, reducing dual-path confusion - Toast error calls are explicit in catch block Test Results: - Frontend: 312/312 tests passing (includes 21 photo upload tests) - Act() warnings: Eliminated - No regressions introduced
This commit is contained in:
@@ -18,25 +18,35 @@ export default function ItemPhotoUpload({
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const cameraInputRef = useRef<HTMLInputElement>(null);
|
||||
const [localError, setLocalError] = useState<string | null>(null);
|
||||
const toastIdRef = useRef<string | null>(null);
|
||||
|
||||
// Update local error when hook error changes
|
||||
// Sync hook error to local state for display
|
||||
React.useEffect(() => {
|
||||
if (error) {
|
||||
setLocalError(error);
|
||||
onError(error);
|
||||
}
|
||||
}, [error, onError]);
|
||||
}, [error]);
|
||||
|
||||
// Cleanup: dismiss pending toasts on unmount
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
if (toastIdRef.current) {
|
||||
toast.dismiss(toastIdRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleFileSelect = async (file: File) => {
|
||||
setLocalError(null);
|
||||
|
||||
if (!file) return;
|
||||
|
||||
const toastId = toast.loading('Uploading...');
|
||||
toastIdRef.current = toast.loading('Uploading...');
|
||||
|
||||
try {
|
||||
const photo = await upload(file, itemId);
|
||||
toast.success('Photo uploaded successfully', { id: toastId });
|
||||
toast.success('Photo uploaded successfully', { id: toastIdRef.current });
|
||||
toastIdRef.current = null;
|
||||
onUploadSuccess(photo);
|
||||
|
||||
// Reset file inputs
|
||||
@@ -49,7 +59,8 @@ export default function ItemPhotoUpload({
|
||||
} catch (err: any) {
|
||||
const errorMsg = err.message || 'Upload failed';
|
||||
setLocalError(errorMsg);
|
||||
toast.error(errorMsg, { id: toastId });
|
||||
toast.error(errorMsg, { id: toastIdRef.current || undefined });
|
||||
toastIdRef.current = null;
|
||||
onError(errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { renderHook, waitFor } from '@testing-library/react'
|
||||
import { renderHook, waitFor, act } from '@testing-library/react'
|
||||
import { usePhotoUpload } from '@/hooks/usePhotoUpload'
|
||||
import * as api from '@/lib/api'
|
||||
|
||||
@@ -36,7 +36,10 @@ describe('usePhotoUpload Hook', () => {
|
||||
const { result } = renderHook(() => usePhotoUpload())
|
||||
|
||||
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' })
|
||||
const photo = await result.current.upload(file, 123)
|
||||
let photo
|
||||
await act(async () => {
|
||||
photo = await result.current.upload(file, 123)
|
||||
})
|
||||
|
||||
expect(photo).toEqual(mockPhoto)
|
||||
expect(result.current.error).toBe(null)
|
||||
@@ -50,16 +53,16 @@ describe('usePhotoUpload Hook', () => {
|
||||
type: 'image/jpeg',
|
||||
})
|
||||
|
||||
try {
|
||||
await result.current.upload(largeFile, 123)
|
||||
} catch (error) {
|
||||
// Expected to throw
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.error).toBeTruthy()
|
||||
expect(result.current.error).toMatch(/File too large/)
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.upload(largeFile, 123)
|
||||
} catch (error) {
|
||||
// Expected to throw
|
||||
}
|
||||
})
|
||||
|
||||
expect(result.current.error).toBeTruthy()
|
||||
expect(result.current.error).toMatch(/File too large/)
|
||||
})
|
||||
|
||||
it('should reject invalid MIME types', async () => {
|
||||
@@ -67,16 +70,16 @@ describe('usePhotoUpload Hook', () => {
|
||||
|
||||
const invalidFile = new File(['test'], 'test.txt', { type: 'text/plain' })
|
||||
|
||||
try {
|
||||
await result.current.upload(invalidFile, 123)
|
||||
} catch (error) {
|
||||
// Expected to throw
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.error).toBeTruthy()
|
||||
expect(result.current.error).toMatch(/Invalid image format/)
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.upload(invalidFile, 123)
|
||||
} catch (error) {
|
||||
// Expected to throw
|
||||
}
|
||||
})
|
||||
|
||||
expect(result.current.error).toBeTruthy()
|
||||
expect(result.current.error).toMatch(/Invalid image format/)
|
||||
})
|
||||
|
||||
it('should set isLoading to true during upload', async () => {
|
||||
@@ -90,14 +93,19 @@ describe('usePhotoUpload Hook', () => {
|
||||
const { result } = renderHook(() => usePhotoUpload())
|
||||
|
||||
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' })
|
||||
const uploadPromiseResult = result.current.upload(file, 123)
|
||||
let uploadPromiseResult
|
||||
await act(async () => {
|
||||
uploadPromiseResult = result.current.upload(file, 123)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(true)
|
||||
})
|
||||
|
||||
resolveUpload({ status: 'ok', photo: {} })
|
||||
await uploadPromiseResult
|
||||
await act(async () => {
|
||||
resolveUpload({ status: 'ok', photo: {} })
|
||||
await uploadPromiseResult
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false)
|
||||
@@ -127,7 +135,10 @@ describe('usePhotoUpload Hook', () => {
|
||||
const { result } = renderHook(() => usePhotoUpload())
|
||||
const file = new File(['test'], `test.${format.ext}`, { type: format.type })
|
||||
|
||||
const photo = await result.current.upload(file, 123)
|
||||
let photo
|
||||
await act(async () => {
|
||||
photo = await result.current.upload(file, 123)
|
||||
})
|
||||
|
||||
expect(photo).toEqual(mockPhoto)
|
||||
expect(result.current.error).toBe(null)
|
||||
@@ -143,16 +154,16 @@ describe('usePhotoUpload Hook', () => {
|
||||
|
||||
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' })
|
||||
|
||||
try {
|
||||
await result.current.upload(file, 123)
|
||||
} catch (error) {
|
||||
// Expected to throw
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.error).toBeTruthy()
|
||||
expect(result.current.error).toMatch(/Network error/)
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.upload(file, 123)
|
||||
} catch (error) {
|
||||
// Expected to throw
|
||||
}
|
||||
})
|
||||
|
||||
expect(result.current.error).toBeTruthy()
|
||||
expect(result.current.error).toMatch(/Network error/)
|
||||
})
|
||||
|
||||
it('should reset error state on successful upload', async () => {
|
||||
@@ -170,7 +181,9 @@ describe('usePhotoUpload Hook', () => {
|
||||
const { result } = renderHook(() => usePhotoUpload())
|
||||
|
||||
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' })
|
||||
await result.current.upload(file, 123)
|
||||
await act(async () => {
|
||||
await result.current.upload(file, 123)
|
||||
})
|
||||
|
||||
expect(result.current.error).toBe(null)
|
||||
})
|
||||
@@ -190,7 +203,10 @@ describe('usePhotoUpload Hook', () => {
|
||||
const { result } = renderHook(() => usePhotoUpload())
|
||||
|
||||
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' })
|
||||
const uploadedPhoto = await result.current.upload(file, 123)
|
||||
let uploadedPhoto
|
||||
await act(async () => {
|
||||
uploadedPhoto = await result.current.upload(file, 123)
|
||||
})
|
||||
|
||||
expect(uploadedPhoto).toHaveProperty('thumbnail_url')
|
||||
expect(uploadedPhoto).toHaveProperty('full_url')
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 541 B |
Binary file not shown.
|
Before Width: | Height: | Size: 541 B |
Binary file not shown.
|
Before Width: | Height: | Size: 361 B |
Binary file not shown.
|
Before Width: | Height: | Size: 541 B |
Reference in New Issue
Block a user