import { renderHook, act, waitFor } from '@testing-library/react-native' import { useTemplateFavorite } from './use-template-favorite' import { root } from '@repo/core' import { TemplateSocialController } from '@repo/sdk' import { handleError } from './use-error' import { templateSocialStore } from '@/stores/templateSocialStore' jest.mock('@repo/core', () => ({ root: { get: jest.fn(), }, })) jest.mock('@/stores/templateSocialStore', () => ({ templateSocialStore: { setFavorited: jest.fn(), setFavoriteCount: jest.fn(), getFavoriteCount: jest.fn(), isFavorited: jest.fn().mockReturnValue(false), }, })) jest.mock('./use-error', () => ({ handleError: jest.fn(async (cb) => { try { const data = await cb() return { data, error: null } } catch (e) { return { data: null, error: e } } }), })) describe('useTemplateFavorite', () => { const mockTemplateId = 'template-123' beforeEach(() => { jest.clearAllMocks() }) afterEach(() => { jest.restoreAllMocks() }) describe('initial state', () => { it('should return initial state with no data', () => { const { result } = renderHook(() => useTemplateFavorite()) expect(result.current.loading).toBe(false) expect(result.current.error).toBeNull() }) it('should accept templateId parameter', () => { const { result } = renderHook(() => useTemplateFavorite(mockTemplateId)) expect(result.current.loading).toBe(false) expect(result.current.error).toBeNull() }) }) describe('favorite function', () => { it('should favorite a template successfully', async () => { const mockSuccessResponse = { success: true } const mockController = { favorite: jest.fn().mockResolvedValue(mockSuccessResponse), } ;(root.get as jest.Mock).mockReturnValue(mockController) ;(handleError as jest.Mock).mockImplementation(async (cb) => { const data = await cb() return { data, error: null } }) const { result } = renderHook(() => useTemplateFavorite(mockTemplateId)) await act(async () => { await result.current.favorite() }) expect(mockController.favorite).toHaveBeenCalledWith({ templateId: mockTemplateId, }) expect(templateSocialStore.setFavorited).toHaveBeenCalledWith(mockTemplateId, true) expect(result.current.loading).toBe(false) expect(result.current.error).toBeNull() }) it('should handle API errors when favoriting', async () => { const mockError = { status: 500, statusText: 'Internal Server Error', message: 'Failed to favorite template', } const mockController = { favorite: jest.fn().mockRejectedValue(mockError), } ;(root.get as jest.Mock).mockReturnValue(mockController) ;(handleError as jest.Mock).mockImplementation(async (cb) => { try { await cb() return { data: null, error: null } } catch (e) { return { data: null, error: e } } }) const { result } = renderHook(() => useTemplateFavorite(mockTemplateId)) await act(async () => { await result.current.favorite() }) expect(result.current.error).toEqual(mockError) // 错误时应该回滚乐观更新,setFavorited 会被调用两次:先 true,后 false expect(templateSocialStore.setFavorited).toHaveBeenLastCalledWith(mockTemplateId, false) expect(result.current.loading).toBe(false) }) it('should set loading to true during favorite operation', async () => { let resolveFavorite: (value: any) => void const favoritePromise = new Promise((resolve) => { resolveFavorite = resolve }) const mockController = { favorite: jest.fn().mockReturnValue(favoritePromise), } ;(root.get as jest.Mock).mockReturnValue(mockController) ;(handleError as jest.Mock).mockImplementation(async (cb) => { const data = await cb() return { data, error: null } }) const { result } = renderHook(() => useTemplateFavorite(mockTemplateId)) act(() => { result.current.favorite() }) expect(result.current.loading).toBe(true) await act(async () => { resolveFavorite!({ success: true }) await favoritePromise }) expect(result.current.loading).toBe(false) }) }) describe('unfavorite function', () => { it('should unfavorite a template successfully', async () => { const mockSuccessResponse = { success: true } const mockController = { unfavorite: jest.fn().mockResolvedValue(mockSuccessResponse), } ;(root.get as jest.Mock).mockReturnValue(mockController) ;(handleError as jest.Mock).mockImplementation(async (cb) => { const data = await cb() return { data, error: null } }) const { result } = renderHook(() => useTemplateFavorite(mockTemplateId, true), // 初始为已收藏状态 ) await act(async () => { await result.current.unfavorite() }) expect(mockController.unfavorite).toHaveBeenCalledWith({ templateId: mockTemplateId, }) expect(templateSocialStore.setFavorited).toHaveBeenCalledWith(mockTemplateId, false) expect(result.current.loading).toBe(false) expect(result.current.error).toBeNull() }) it('should handle API errors when unfavoriting', async () => { const mockError = { status: 500, message: 'Failed to unfavorite template', } const mockController = { unfavorite: jest.fn().mockRejectedValue(mockError), } ;(root.get as jest.Mock).mockReturnValue(mockController) ;(handleError as jest.Mock).mockImplementation(async (cb) => { try { await cb() return { data: null, error: null } } catch (e) { return { data: null, error: e } } }) const { result } = renderHook(() => useTemplateFavorite(mockTemplateId, true), ) await act(async () => { await result.current.unfavorite() }) expect(result.current.error).toEqual(mockError) expect(result.current.loading).toBe(false) }) it('should set loading to true during unfavorite operation', async () => { let resolveUnfavorite: (value: any) => void const unfavoritePromise = new Promise((resolve) => { resolveUnfavorite = resolve }) const mockController = { unfavorite: jest.fn().mockReturnValue(unfavoritePromise), } ;(root.get as jest.Mock).mockReturnValue(mockController) ;(handleError as jest.Mock).mockImplementation(async (cb) => { const data = await cb() return { data, error: null } }) const { result } = renderHook(() => useTemplateFavorite(mockTemplateId, true), ) act(() => { result.current.unfavorite() }) expect(result.current.loading).toBe(true) await act(async () => { resolveUnfavorite!({ success: true }) await unfavoritePromise }) expect(result.current.loading).toBe(false) }) }) describe('checkFavorited function', () => { it('should check favorited status successfully', async () => { const mockResponse = { favorited: true } const mockController = { checkFavorited: jest.fn().mockResolvedValue(mockResponse), } ;(root.get as jest.Mock).mockReturnValue(mockController) ;(handleError as jest.Mock).mockImplementation(async (cb) => { const data = await cb() return { data, error: null } }) const { result } = renderHook(() => useTemplateFavorite(mockTemplateId)) await act(async () => { await result.current.checkFavorited() }) expect(mockController.checkFavorited).toHaveBeenCalledWith({ templateId: mockTemplateId, }) expect(templateSocialStore.setFavorited).toHaveBeenCalledWith(mockTemplateId, true) expect(result.current.loading).toBe(false) expect(result.current.error).toBeNull() }) it('should handle API errors when checking favorited status', async () => { const mockError = { status: 500, message: 'Failed to check favorited status', } const mockController = { checkFavorited: jest.fn().mockRejectedValue(mockError), } ;(root.get as jest.Mock).mockReturnValue(mockController) ;(handleError as jest.Mock).mockImplementation(async (cb) => { try { await cb() return { data: null, error: null } } catch (e) { return { data: null, error: e } } }) const { result } = renderHook(() => useTemplateFavorite(mockTemplateId)) await act(async () => { await result.current.checkFavorited() }) expect(result.current.error).toEqual(mockError) expect(result.current.loading).toBe(false) }) it('should update favorited state based on response', async () => { const mockResponse = { favorited: false } const mockController = { checkFavorited: jest.fn().mockResolvedValue(mockResponse), } ;(root.get as jest.Mock).mockReturnValue(mockController) ;(handleError as jest.Mock).mockImplementation(async (cb) => { const data = await cb() return { data, error: null } }) const { result } = renderHook(() => useTemplateFavorite(mockTemplateId, true), // 初始为已收藏 ) await act(async () => { await result.current.checkFavorited() }) expect(templateSocialStore.setFavorited).toHaveBeenCalledWith(mockTemplateId, false) }) }) describe('edge cases', () => { it('should not call API when templateId is not provided', async () => { const mockController = { favorite: jest.fn(), unfavorite: jest.fn(), checkFavorited: jest.fn(), } ;(root.get as jest.Mock).mockReturnValue(mockController) const { result } = renderHook(() => useTemplateFavorite()) await act(async () => { await result.current.favorite() await result.current.unfavorite() await result.current.checkFavorited() }) expect(mockController.favorite).not.toHaveBeenCalled() expect(mockController.unfavorite).not.toHaveBeenCalled() expect(mockController.checkFavorited).not.toHaveBeenCalled() }) it('should clear error on successful operation', async () => { const mockError = { status: 500, message: 'Error' } const mockSuccessResponse = { success: true } const mockController = { favorite: jest.fn() .mockRejectedValueOnce(mockError) .mockResolvedValueOnce(mockSuccessResponse), } ;(root.get as jest.Mock).mockReturnValue(mockController) ;(handleError as jest.Mock).mockImplementation(async (cb) => { try { const data = await cb() return { data, error: null } } catch (e) { return { data: null, error: e } } }) const { result } = renderHook(() => useTemplateFavorite(mockTemplateId)) await act(async () => { await result.current.favorite() }) expect(result.current.error).toEqual(mockError) await act(async () => { await result.current.favorite() }) expect(result.current.error).toBeNull() }) }) })