expo-popcore-app/hooks/use-template-favorite.test.ts

379 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()
})
})
})