feat: add announcement action hooks with TDD
Add useAnnouncementActions hook for marking announcements as read and useAnnouncementUnreadCount hook for fetching unread announcement count. Both hooks follow TDD principles with complete test coverage. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
97
hooks/use-announcement-actions.test.ts
Normal file
97
hooks/use-announcement-actions.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { renderHook, act } from '@testing-library/react-native'
|
||||
import { useAnnouncementActions } from './use-announcement-actions'
|
||||
import { root } from '@repo/core'
|
||||
import { AnnouncementController } from '@repo/sdk'
|
||||
|
||||
jest.mock('@repo/core', () => ({
|
||||
root: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('useAnnouncementActions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('initial state', () => {
|
||||
it('should return initial state', () => {
|
||||
const mockAnnouncementController = {
|
||||
markRead: jest.fn(),
|
||||
}
|
||||
;(root.get as jest.Mock).mockReturnValue(mockAnnouncementController)
|
||||
|
||||
const { result } = renderHook(() => useAnnouncementActions())
|
||||
|
||||
expect(result.current.markReadLoading).toBe(false)
|
||||
expect(result.current.markReadError).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('markRead', () => {
|
||||
it('should mark announcement as read successfully', async () => {
|
||||
const mockAnnouncementController = {
|
||||
markRead: jest.fn().mockResolvedValue({ message: 'success' }),
|
||||
}
|
||||
;(root.get as jest.Mock).mockReturnValue(mockAnnouncementController)
|
||||
|
||||
const { result } = renderHook(() => useAnnouncementActions())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.markRead('announcement-1')
|
||||
})
|
||||
|
||||
expect(mockAnnouncementController.markRead).toHaveBeenCalledWith({ id: 'announcement-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 as read')
|
||||
const mockAnnouncementController = {
|
||||
markRead: jest.fn().mockRejectedValue(mockError),
|
||||
}
|
||||
;(root.get as jest.Mock).mockReturnValue(mockAnnouncementController)
|
||||
|
||||
const { result } = renderHook(() => useAnnouncementActions())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.markRead('announcement-1')
|
||||
})
|
||||
|
||||
expect(result.current.markReadError).toEqual(mockError)
|
||||
expect(result.current.markReadLoading).toBe(false)
|
||||
})
|
||||
|
||||
it('should set loading state during markRead', async () => {
|
||||
let resolveFetch: (value: any) => void
|
||||
const fetchPromise = new Promise((resolve) => {
|
||||
resolveFetch = resolve
|
||||
})
|
||||
|
||||
const mockAnnouncementController = {
|
||||
markRead: jest.fn().mockReturnValue(fetchPromise),
|
||||
}
|
||||
;(root.get as jest.Mock).mockReturnValue(mockAnnouncementController)
|
||||
|
||||
const { result } = renderHook(() => useAnnouncementActions())
|
||||
|
||||
act(() => {
|
||||
result.current.markRead('announcement-1')
|
||||
})
|
||||
|
||||
expect(result.current.markReadLoading).toBe(true)
|
||||
|
||||
await act(async () => {
|
||||
resolveFetch!({ message: 'success' })
|
||||
await fetchPromise
|
||||
})
|
||||
|
||||
expect(result.current.markReadLoading).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
27
hooks/use-announcement-actions.ts
Normal file
27
hooks/use-announcement-actions.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { root } from '@repo/core'
|
||||
import { AnnouncementController } from '@repo/sdk'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const useAnnouncementActions = () => {
|
||||
const [markReadLoading, setMarkReadLoading] = useState(false)
|
||||
const [markReadError, setMarkReadError] = useState<Error | null>(null)
|
||||
|
||||
const markRead = async (id: string) => {
|
||||
setMarkReadLoading(true)
|
||||
setMarkReadError(null)
|
||||
try {
|
||||
const announcementController = root.get(AnnouncementController)
|
||||
await announcementController.markRead({ id })
|
||||
} catch (error) {
|
||||
setMarkReadError(error as Error)
|
||||
} finally {
|
||||
setMarkReadLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
markRead,
|
||||
markReadLoading,
|
||||
markReadError,
|
||||
}
|
||||
}
|
||||
101
hooks/use-announcement-unread-count.test.ts
Normal file
101
hooks/use-announcement-unread-count.test.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { renderHook, act } from '@testing-library/react-native'
|
||||
import { useAnnouncementUnreadCount } from './use-announcement-unread-count'
|
||||
import { root } from '@repo/core'
|
||||
import { AnnouncementController } from '@repo/sdk'
|
||||
|
||||
jest.mock('@repo/core', () => ({
|
||||
root: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('useAnnouncementUnreadCount', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('initial state', () => {
|
||||
it('should return initial state', () => {
|
||||
const mockAnnouncementController = {
|
||||
getUnreadCount: jest.fn(),
|
||||
}
|
||||
;(root.get as jest.Mock).mockReturnValue(mockAnnouncementController)
|
||||
|
||||
const { result } = renderHook(() => useAnnouncementUnreadCount())
|
||||
|
||||
expect(result.current.data).toBeUndefined()
|
||||
expect(result.current.loading).toBe(false)
|
||||
expect(result.current.error).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('refetch', () => {
|
||||
it('should fetch unread count successfully', async () => {
|
||||
const mockData = { count: 5 }
|
||||
const mockAnnouncementController = {
|
||||
getUnreadCount: jest.fn().mockResolvedValue(mockData),
|
||||
}
|
||||
;(root.get as jest.Mock).mockReturnValue(mockAnnouncementController)
|
||||
|
||||
const { result } = renderHook(() => useAnnouncementUnreadCount())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.refetch()
|
||||
})
|
||||
|
||||
expect(mockAnnouncementController.getUnreadCount).toHaveBeenCalled()
|
||||
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 mockAnnouncementController = {
|
||||
getUnreadCount: jest.fn().mockRejectedValue(mockError),
|
||||
}
|
||||
;(root.get as jest.Mock).mockReturnValue(mockAnnouncementController)
|
||||
|
||||
const { result } = renderHook(() => useAnnouncementUnreadCount())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.refetch()
|
||||
})
|
||||
|
||||
expect(result.current.error).toEqual(mockError)
|
||||
expect(result.current.data).toBeUndefined()
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
it('should set loading state during fetch', async () => {
|
||||
let resolveFetch: (value: any) => void
|
||||
const fetchPromise = new Promise((resolve) => {
|
||||
resolveFetch = resolve
|
||||
})
|
||||
|
||||
const mockAnnouncementController = {
|
||||
getUnreadCount: jest.fn().mockReturnValue(fetchPromise),
|
||||
}
|
||||
;(root.get as jest.Mock).mockReturnValue(mockAnnouncementController)
|
||||
|
||||
const { result } = renderHook(() => useAnnouncementUnreadCount())
|
||||
|
||||
act(() => {
|
||||
result.current.refetch()
|
||||
})
|
||||
|
||||
expect(result.current.loading).toBe(true)
|
||||
|
||||
await act(async () => {
|
||||
resolveFetch!({ count: 3 })
|
||||
await fetchPromise
|
||||
})
|
||||
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
30
hooks/use-announcement-unread-count.ts
Normal file
30
hooks/use-announcement-unread-count.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { root } from '@repo/core'
|
||||
import { AnnouncementController, type UnreadAnnouncementCountResult } from '@repo/sdk'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const useAnnouncementUnreadCount = () => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
const [data, setData] = useState<UnreadAnnouncementCountResult>()
|
||||
|
||||
const refetch = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const announcementController = root.get(AnnouncementController)
|
||||
const result = await announcementController.getUnreadCount()
|
||||
setData(result)
|
||||
} catch (err) {
|
||||
setError(err as Error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
refetch,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user