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:
2026-04-21 12:50:44 +03:00
parent db9aafd47f
commit 5a64dadc1e
7 changed files with 69 additions and 42 deletions

View File

@@ -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')