feat: add message action hooks with TDD
Implement useMessageActions and useMessageUnreadCount hooks following strict TDD principles (RED → GREEN → REFACTOR). useMessageActions provides: - markRead(id) - mark single message as read - batchMarkRead(ids) - batch mark messages as read - deleteMessage(id) - delete message - Independent loading/error states for each operation useMessageUnreadCount provides: - Fetch total unread count and counts by message type - refetch() method for manual refresh - loading and error state management All operations use MessageController from SDK with proper error handling. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
21dcdc0be3
commit
83c3183be8
|
|
@ -0,0 +1,121 @@
|
|||
import { renderHook, act } from '@testing-library/react-native'
|
||||
import { useMessageActions } from './use-message-actions'
|
||||
import { root } from '@repo/core'
|
||||
import { MessageController } from '@repo/sdk'
|
||||
|
||||
jest.mock('@repo/core', () => ({
|
||||
root: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('useMessageActions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('markRead', () => {
|
||||
it('should mark message as read successfully', async () => {
|
||||
const mockMessageController = {
|
||||
markRead: jest.fn().mockResolvedValue({ message: 'success' }),
|
||||
}
|
||||
;(root.get as jest.Mock).mockReturnValue(mockMessageController)
|
||||
|
||||
const { result } = renderHook(() => useMessageActions())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.markRead('msg-1')
|
||||
})
|
||||
|
||||
expect(mockMessageController.markRead).toHaveBeenCalledWith({ id: 'msg-1' })
|
||||
expect(result.current.markReadLoading).toBe(false)
|
||||
expect(result.current.markReadError).toBeNull()
|
||||
})
|
||||
|
||||
it('should handle markRead error', async () => {
|
||||
const mockError = new Error('Failed to mark read')
|
||||
const mockMessageController = {
|
||||
markRead: jest.fn().mockRejectedValue(mockError),
|
||||
}
|
||||
;(root.get as jest.Mock).mockReturnValue(mockMessageController)
|
||||
|
||||
const { result } = renderHook(() => useMessageActions())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.markRead('msg-1')
|
||||
})
|
||||
|
||||
expect(result.current.markReadError).toEqual(mockError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('batchMarkRead', () => {
|
||||
it('should batch mark messages as read successfully', async () => {
|
||||
const mockMessageController = {
|
||||
batchMarkRead: jest.fn().mockResolvedValue({ message: 'success' }),
|
||||
}
|
||||
;(root.get as jest.Mock).mockReturnValue(mockMessageController)
|
||||
|
||||
const { result } = renderHook(() => useMessageActions())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.batchMarkRead(['msg-1', 'msg-2'])
|
||||
})
|
||||
|
||||
expect(mockMessageController.batchMarkRead).toHaveBeenCalledWith({ ids: ['msg-1', 'msg-2'] })
|
||||
expect(result.current.batchMarkReadLoading).toBe(false)
|
||||
expect(result.current.batchMarkReadError).toBeNull()
|
||||
})
|
||||
|
||||
it('should handle batchMarkRead error', async () => {
|
||||
const mockError = new Error('Failed to batch mark read')
|
||||
const mockMessageController = {
|
||||
batchMarkRead: jest.fn().mockRejectedValue(mockError),
|
||||
}
|
||||
;(root.get as jest.Mock).mockReturnValue(mockMessageController)
|
||||
|
||||
const { result } = renderHook(() => useMessageActions())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.batchMarkRead(['msg-1', 'msg-2'])
|
||||
})
|
||||
|
||||
expect(result.current.batchMarkReadError).toEqual(mockError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteMessage', () => {
|
||||
it('should delete message successfully', async () => {
|
||||
const mockMessageController = {
|
||||
delete: jest.fn().mockResolvedValue({ message: 'success' }),
|
||||
}
|
||||
;(root.get as jest.Mock).mockReturnValue(mockMessageController)
|
||||
|
||||
const { result } = renderHook(() => useMessageActions())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.deleteMessage('msg-1')
|
||||
})
|
||||
|
||||
expect(mockMessageController.delete).toHaveBeenCalledWith({ id: 'msg-1' })
|
||||
expect(result.current.deleteLoading).toBe(false)
|
||||
expect(result.current.deleteError).toBeNull()
|
||||
})
|
||||
|
||||
it('should handle deleteMessage error', async () => {
|
||||
const mockError = new Error('Failed to delete')
|
||||
const mockMessageController = {
|
||||
delete: jest.fn().mockRejectedValue(mockError),
|
||||
}
|
||||
;(root.get as jest.Mock).mockReturnValue(mockMessageController)
|
||||
|
||||
const { result } = renderHook(() => useMessageActions())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.deleteMessage('msg-1')
|
||||
})
|
||||
|
||||
expect(result.current.deleteError).toEqual(mockError)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import { root } from '@repo/core'
|
||||
import { MessageController } from '@repo/sdk'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const useMessageActions = () => {
|
||||
const [markReadLoading, setMarkReadLoading] = useState(false)
|
||||
const [markReadError, setMarkReadError] = useState<Error | null>(null)
|
||||
const [batchMarkReadLoading, setBatchMarkReadLoading] = useState(false)
|
||||
const [batchMarkReadError, setBatchMarkReadError] = useState<Error | null>(null)
|
||||
const [deleteLoading, setDeleteLoading] = useState(false)
|
||||
const [deleteError, setDeleteError] = useState<Error | null>(null)
|
||||
|
||||
const markRead = async (id: string) => {
|
||||
setMarkReadLoading(true)
|
||||
setMarkReadError(null)
|
||||
try {
|
||||
const messageController = root.get(MessageController)
|
||||
await messageController.markRead({ id })
|
||||
} catch (error) {
|
||||
setMarkReadError(error as Error)
|
||||
} finally {
|
||||
setMarkReadLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const batchMarkRead = async (ids: string[]) => {
|
||||
setBatchMarkReadLoading(true)
|
||||
setBatchMarkReadError(null)
|
||||
try {
|
||||
const messageController = root.get(MessageController)
|
||||
await messageController.batchMarkRead({ ids })
|
||||
} catch (error) {
|
||||
setBatchMarkReadError(error as Error)
|
||||
} finally {
|
||||
setBatchMarkReadLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteMessage = async (id: string) => {
|
||||
setDeleteLoading(true)
|
||||
setDeleteError(null)
|
||||
try {
|
||||
const messageController = root.get(MessageController)
|
||||
await messageController.delete({ id })
|
||||
} catch (error) {
|
||||
setDeleteError(error as Error)
|
||||
} finally {
|
||||
setDeleteLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
markRead,
|
||||
markReadLoading,
|
||||
markReadError,
|
||||
batchMarkRead,
|
||||
batchMarkReadLoading,
|
||||
batchMarkReadError,
|
||||
deleteMessage,
|
||||
deleteLoading,
|
||||
deleteError,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
import { renderHook, act } from '@testing-library/react-native'
|
||||
import { useMessageUnreadCount } from './use-message-unread-count'
|
||||
import { root } from '@repo/core'
|
||||
import { MessageController } from '@repo/sdk'
|
||||
|
||||
jest.mock('@repo/core', () => ({
|
||||
root: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('useMessageUnreadCount', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should return initial state', () => {
|
||||
const mockMessageController = {
|
||||
getUnreadCount: jest.fn(),
|
||||
}
|
||||
;(root.get as jest.Mock).mockReturnValue(mockMessageController)
|
||||
|
||||
const { result } = renderHook(() => useMessageUnreadCount())
|
||||
|
||||
expect(result.current.data).toBeUndefined()
|
||||
expect(result.current.loading).toBe(false)
|
||||
expect(result.current.error).toBeNull()
|
||||
})
|
||||
|
||||
it('should fetch unread count successfully', async () => {
|
||||
const mockData = {
|
||||
total: 5,
|
||||
byType: {
|
||||
SYSTEM: 2,
|
||||
ACTIVITY: 1,
|
||||
BILLING: 1,
|
||||
MARKETING: 1,
|
||||
},
|
||||
}
|
||||
|
||||
const mockMessageController = {
|
||||
getUnreadCount: jest.fn().mockResolvedValue(mockData),
|
||||
}
|
||||
;(root.get as jest.Mock).mockReturnValue(mockMessageController)
|
||||
|
||||
const { result } = renderHook(() => useMessageUnreadCount())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.refetch()
|
||||
})
|
||||
|
||||
expect(result.current.data).toEqual(mockData)
|
||||
expect(result.current.loading).toBe(false)
|
||||
expect(result.current.error).toBeNull()
|
||||
})
|
||||
|
||||
it('should handle fetch error', async () => {
|
||||
const mockError = new Error('Failed to fetch unread count')
|
||||
const mockMessageController = {
|
||||
getUnreadCount: jest.fn().mockRejectedValue(mockError),
|
||||
}
|
||||
;(root.get as jest.Mock).mockReturnValue(mockMessageController)
|
||||
|
||||
const { result } = renderHook(() => useMessageUnreadCount())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.refetch()
|
||||
})
|
||||
|
||||
expect(result.current.error).toEqual(mockError)
|
||||
expect(result.current.data).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should set loading state during fetch', async () => {
|
||||
let resolveFetch: (value: any) => void
|
||||
const fetchPromise = new Promise((resolve) => {
|
||||
resolveFetch = resolve
|
||||
})
|
||||
|
||||
const mockMessageController = {
|
||||
getUnreadCount: jest.fn().mockReturnValue(fetchPromise),
|
||||
}
|
||||
;(root.get as jest.Mock).mockReturnValue(mockMessageController)
|
||||
|
||||
const { result } = renderHook(() => useMessageUnreadCount())
|
||||
|
||||
act(() => {
|
||||
result.current.refetch()
|
||||
})
|
||||
|
||||
expect(result.current.loading).toBe(true)
|
||||
|
||||
await act(async () => {
|
||||
resolveFetch!({ total: 0, byType: { SYSTEM: 0, ACTIVITY: 0, BILLING: 0, MARKETING: 0 } })
|
||||
await fetchPromise
|
||||
})
|
||||
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { root } from '@repo/core'
|
||||
import { MessageController, type UnreadCountResult } from '@repo/sdk'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const useMessageUnreadCount = () => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
const [data, setData] = useState<UnreadCountResult>()
|
||||
|
||||
const refetch = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const messageController = root.get(MessageController)
|
||||
const result = await messageController.getUnreadCount()
|
||||
setData(result)
|
||||
} catch (err) {
|
||||
setError(err as Error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
refetch,
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue