fix: type error

This commit is contained in:
imeepos
2026-01-29 18:46:34 +08:00
parent 5a4d1ac1bd
commit 8f33ade8df
13 changed files with 102 additions and 163 deletions

View File

@@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import { render, waitFor } from '@testing-library/react-native' import { render, waitFor } from '@testing-library/react-native'
import FavoritesScreen from '../favorites' import FavoritesScreen from '../../favorites'
// Mock react-native-safe-area-context // Mock react-native-safe-area-context
jest.mock('react-native-safe-area-context', () => ({ jest.mock('react-native-safe-area-context', () => ({

View File

@@ -22,7 +22,7 @@ export default function TabLayout() {
const { data: announcementData, refetch: refetchAnnouncements } = useAnnouncementUnreadCount() const { data: announcementData, refetch: refetchAnnouncements } = useAnnouncementUnreadCount()
// 计算总未读数 // 计算总未读数
const totalUnreadCount = (messageData?.count || 0) + (announcementData?.count || 0) const totalUnreadCount = (messageData?.total || 0) + (announcementData?.count || 0)
// 组件挂载时获取未读数 // 组件挂载时获取未读数
useEffect(() => { useEffect(() => {

View File

@@ -178,13 +178,27 @@ export default function MessageScreen() {
} }
}, [hasMore, loadingMore, loading, loadMore]) }, [hasMore, loadingMore, loading, loadMore])
// Convert Message to MessageCardMessage format
const convertToMessageCardFormat = (message: Message): MessageCardMessage => ({
id: message.id,
type: message.type,
title: message.title,
content: message.content,
data: message.data,
link: message.link,
priority: message.priority,
isRead: message.isRead,
readAt: message.readAt ? (typeof message.readAt === 'string' ? message.readAt : message.readAt.toISOString()) : undefined,
createdAt: message.createdAt,
})
// Render message item // Render message item
const renderMessageItem = useCallback(({ item }: { item: Message }) => ( const renderMessageItem = useCallback(({ item }: { item: Message }) => (
<MessageCard <MessageCard
message={item} message={convertToMessageCardFormat(item)}
onPress={handleMessagePress} onPress={() => handleMessagePress(item)}
/> />
), [handleDeleteMessage, handleMessagePress]) ), [handleMessagePress])
// Render list footer // Render list footer
const renderFooter = useCallback(() => { const renderFooter = useCallback(() => {

View File

@@ -253,7 +253,7 @@ export default function VideoScreen() {
}, [router]) }, [router])
const handleSwipeRight = useCallback(() => { const handleSwipeRight = useCallback(() => {
router.push('/(tabs)/') router.push('/(tabs)')
}, [router]) }, [router])
const { handleGestureEvent, handleGestureStateChange } = useSwipeNavigation({ const { handleGestureEvent, handleGestureStateChange } = useSwipeNavigation({

View File

@@ -82,7 +82,7 @@ describe('TemplateCard Component', () => {
it('should display Chinese title when language is zh-CN', () => { it('should display Chinese title when language is zh-CN', () => {
const title = '测试模板' const title = '测试模板'
const titleEn = 'Test Template' const titleEn = 'Test Template'
const language = 'zh-CN' const language: string = 'zh-CN'
// 根据语言选择显示的标题 // 根据语言选择显示的标题
const displayTitle = language === 'en-US' ? titleEn : title const displayTitle = language === 'en-US' ? titleEn : title

View File

@@ -1,14 +1,7 @@
import { renderHook, act } from '@testing-library/react-native' import { renderHook, act } from '@testing-library/react-native'
import { useDownloadMedia } from './use-download-media' import { useDownloadMedia } from './use-download-media'
import * as FileSystem from 'expo-file-system'
import * as MediaLibrary from 'expo-media-library' import * as MediaLibrary from 'expo-media-library'
jest.mock('expo-file-system', () => ({
documentDirectory: 'file:///mock/document/',
createDownloadResumable: jest.fn(),
deleteAsync: jest.fn(),
}))
jest.mock('expo-media-library', () => ({ jest.mock('expo-media-library', () => ({
requestPermissionsAsync: jest.fn(), requestPermissionsAsync: jest.fn(),
saveToLibraryAsync: jest.fn(), saveToLibraryAsync: jest.fn(),
@@ -56,13 +49,7 @@ describe('useDownloadMedia', () => {
;(MediaLibrary.requestPermissionsAsync as jest.Mock).mockResolvedValue({ ;(MediaLibrary.requestPermissionsAsync as jest.Mock).mockResolvedValue({
status: 'granted', status: 'granted',
}) })
const mockDownloadAsync = jest.fn().mockResolvedValue({ uri: 'file:///mock/document/download_123.jpg' })
;(FileSystem.createDownloadResumable as jest.Mock).mockReturnValue({
downloadAsync: mockDownloadAsync,
})
;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockResolvedValue(undefined) ;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockResolvedValue(undefined)
;(FileSystem.deleteAsync as jest.Mock).mockResolvedValue(undefined)
const { result } = renderHook(() => useDownloadMedia()) const { result } = renderHook(() => useDownloadMedia())
@@ -84,12 +71,7 @@ describe('useDownloadMedia', () => {
}) })
it('should download image successfully', async () => { it('should download image successfully', async () => {
const mockDownloadAsync = jest.fn().mockResolvedValue({ uri: 'file:///mock/document/download_123.jpg' })
;(FileSystem.createDownloadResumable as jest.Mock).mockReturnValue({
downloadAsync: mockDownloadAsync,
})
;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockResolvedValue(undefined) ;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockResolvedValue(undefined)
;(FileSystem.deleteAsync as jest.Mock).mockResolvedValue(undefined)
const { result } = renderHook(() => useDownloadMedia()) const { result } = renderHook(() => useDownloadMedia())
@@ -99,26 +81,11 @@ describe('useDownloadMedia', () => {
}) })
expect(response).toEqual({ success: true }) expect(response).toEqual({ success: true })
expect(FileSystem.createDownloadResumable).toHaveBeenCalledWith( expect(MediaLibrary.saveToLibraryAsync).toHaveBeenCalledWith('https://example.com/image.jpg')
'https://example.com/image.jpg',
expect.stringMatching(/^file:\/\/\/mock\/document\/download_\d+\.jpg$/),
{},
expect.any(Function)
)
expect(MediaLibrary.saveToLibraryAsync).toHaveBeenCalled()
expect(FileSystem.deleteAsync).toHaveBeenCalledWith(
expect.stringMatching(/^file:\/\/\/mock\/document\/download_\d+\.jpg$/),
{ idempotent: true }
)
}) })
it('should download video successfully', async () => { it('should download video successfully', async () => {
const mockDownloadAsync = jest.fn().mockResolvedValue({ uri: 'file:///mock/document/download_123.mp4' })
;(FileSystem.createDownloadResumable as jest.Mock).mockReturnValue({
downloadAsync: mockDownloadAsync,
})
;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockResolvedValue(undefined) ;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockResolvedValue(undefined)
;(FileSystem.deleteAsync as jest.Mock).mockResolvedValue(undefined)
const { result } = renderHook(() => useDownloadMedia()) const { result } = renderHook(() => useDownloadMedia())
@@ -128,19 +95,11 @@ describe('useDownloadMedia', () => {
}) })
expect(response).toEqual({ success: true }) expect(response).toEqual({ success: true })
expect(FileSystem.createDownloadResumable).toHaveBeenCalledWith( expect(MediaLibrary.saveToLibraryAsync).toHaveBeenCalledWith('https://example.com/video.mp4')
'https://example.com/video.mp4',
expect.stringMatching(/^file:\/\/\/mock\/document\/download_\d+\.mp4$/),
{},
expect.any(Function)
)
}) })
it('should handle download failure', async () => { it('should handle download failure', async () => {
const mockDownloadAsync = jest.fn().mockRejectedValue(new Error('Network error')) ;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockRejectedValue(new Error('Network error'))
;(FileSystem.createDownloadResumable as jest.Mock).mockReturnValue({
downloadAsync: mockDownloadAsync,
})
const { result } = renderHook(() => useDownloadMedia()) const { result } = renderHook(() => useDownloadMedia())
@@ -154,12 +113,7 @@ describe('useDownloadMedia', () => {
}) })
it('should handle save to library failure', async () => { it('should handle save to library failure', async () => {
const mockDownloadAsync = jest.fn().mockResolvedValue({ uri: 'file:///mock/document/download_123.jpg' })
;(FileSystem.createDownloadResumable as jest.Mock).mockReturnValue({
downloadAsync: mockDownloadAsync,
})
;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockRejectedValue(new Error('Save failed')) ;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockRejectedValue(new Error('Save failed'))
;(FileSystem.deleteAsync as jest.Mock).mockResolvedValue(undefined)
const { result } = renderHook(() => useDownloadMedia()) const { result } = renderHook(() => useDownloadMedia())
@@ -170,8 +124,6 @@ describe('useDownloadMedia', () => {
expect(response).toEqual({ success: false, error: 'Save failed' }) expect(response).toEqual({ success: false, error: 'Save failed' })
expect(result.current.error).toBe('Save failed') expect(result.current.error).toBe('Save failed')
// Should still attempt to clean up temp file
expect(FileSystem.deleteAsync).toHaveBeenCalled()
}) })
}) })
@@ -183,17 +135,12 @@ describe('useDownloadMedia', () => {
}) })
it('should set loading to true during download', async () => { it('should set loading to true during download', async () => {
let resolveDownload: (value: any) => void let resolveSave: (value: any) => void
const downloadPromise = new Promise((resolve) => { const savePromise = new Promise((resolve) => {
resolveDownload = resolve resolveSave = resolve
}) })
const mockDownloadAsync = jest.fn().mockReturnValue(downloadPromise) ;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockReturnValue(savePromise)
;(FileSystem.createDownloadResumable as jest.Mock).mockReturnValue({
downloadAsync: mockDownloadAsync,
})
;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockResolvedValue(undefined)
;(FileSystem.deleteAsync as jest.Mock).mockResolvedValue(undefined)
const { result } = renderHook(() => useDownloadMedia()) const { result } = renderHook(() => useDownloadMedia())
@@ -204,18 +151,15 @@ describe('useDownloadMedia', () => {
expect(result.current.loading).toBe(true) expect(result.current.loading).toBe(true)
await act(async () => { await act(async () => {
resolveDownload!({ uri: 'file:///mock/document/download_123.jpg' }) resolveSave!(undefined)
await downloadPromise await savePromise
}) })
expect(result.current.loading).toBe(false) expect(result.current.loading).toBe(false)
}) })
it('should set loading to false after error', async () => { it('should set loading to false after error', async () => {
const mockDownloadAsync = jest.fn().mockRejectedValue(new Error('Error')) ;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockRejectedValue(new Error('Error'))
;(FileSystem.createDownloadResumable as jest.Mock).mockReturnValue({
downloadAsync: mockDownloadAsync,
})
const { result } = renderHook(() => useDownloadMedia()) const { result } = renderHook(() => useDownloadMedia())
@@ -234,23 +178,8 @@ describe('useDownloadMedia', () => {
}) })
}) })
it('should update progress during download', async () => { it('should reset progress after completion', async () => {
let progressCallback: (progress: { totalBytesWritten: number; totalBytesExpectedToWrite: number }) => void
const mockDownloadAsync = jest.fn().mockImplementation(async () => {
// Simulate progress updates
progressCallback({ totalBytesWritten: 50, totalBytesExpectedToWrite: 100 })
return { uri: 'file:///mock/document/download_123.jpg' }
})
;(FileSystem.createDownloadResumable as jest.Mock).mockImplementation(
(url, fileUri, options, callback) => {
progressCallback = callback
return { downloadAsync: mockDownloadAsync }
}
)
;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockResolvedValue(undefined) ;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockResolvedValue(undefined)
;(FileSystem.deleteAsync as jest.Mock).mockResolvedValue(undefined)
const { result } = renderHook(() => useDownloadMedia()) const { result } = renderHook(() => useDownloadMedia())
@@ -263,12 +192,7 @@ describe('useDownloadMedia', () => {
}) })
it('should reset progress on new download', async () => { it('should reset progress on new download', async () => {
const mockDownloadAsync = jest.fn().mockResolvedValue({ uri: 'file:///mock/document/download_123.jpg' })
;(FileSystem.createDownloadResumable as jest.Mock).mockReturnValue({
downloadAsync: mockDownloadAsync,
})
;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockResolvedValue(undefined) ;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockResolvedValue(undefined)
;(FileSystem.deleteAsync as jest.Mock).mockResolvedValue(undefined)
const { result } = renderHook(() => useDownloadMedia()) const { result } = renderHook(() => useDownloadMedia())
@@ -292,12 +216,7 @@ describe('useDownloadMedia', () => {
.mockResolvedValueOnce({ status: 'denied' }) .mockResolvedValueOnce({ status: 'denied' })
.mockResolvedValueOnce({ status: 'granted' }) .mockResolvedValueOnce({ status: 'granted' })
const mockDownloadAsync = jest.fn().mockResolvedValue({ uri: 'file:///mock/document/download_123.jpg' })
;(FileSystem.createDownloadResumable as jest.Mock).mockReturnValue({
downloadAsync: mockDownloadAsync,
})
;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockResolvedValue(undefined) ;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockResolvedValue(undefined)
;(FileSystem.deleteAsync as jest.Mock).mockResolvedValue(undefined)
const { result } = renderHook(() => useDownloadMedia()) const { result } = renderHook(() => useDownloadMedia())
@@ -319,10 +238,7 @@ describe('useDownloadMedia', () => {
status: 'granted', status: 'granted',
}) })
const mockDownloadAsync = jest.fn().mockRejectedValue('Unknown error') ;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockRejectedValue('Unknown error')
;(FileSystem.createDownloadResumable as jest.Mock).mockReturnValue({
downloadAsync: mockDownloadAsync,
})
const { result } = renderHook(() => useDownloadMedia()) const { result } = renderHook(() => useDownloadMedia())

View File

@@ -1,5 +1,4 @@
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import * as FileSystem from 'expo-file-system'
import * as MediaLibrary from 'expo-media-library' import * as MediaLibrary from 'expo-media-library'
export type MediaType = 'image' | 'video' export type MediaType = 'image' | 'video'
@@ -9,6 +8,26 @@ export interface DownloadResult {
error?: string error?: string
} }
// 使用 fetch 下载文件并转换为 base64
const downloadToCache = async (url: string, fileName: string): Promise<string> => {
const response = await fetch(url)
if (!response.ok) {
throw new Error('Download failed')
}
const blob = await response.blob()
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onloadend = () => {
const base64data = reader.result as string
resolve(base64data)
}
reader.onerror = reject
reader.readAsDataURL(blob)
})
}
export const useDownloadMedia = () => { export const useDownloadMedia = () => {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
@@ -19,8 +38,6 @@ export const useDownloadMedia = () => {
setError(null) setError(null)
setProgress(0) setProgress(0)
let fileUri: string | null = null
try { try {
// 1. 请求媒体库权限 // 1. 请求媒体库权限
const { status } = await MediaLibrary.requestPermissionsAsync() const { status } = await MediaLibrary.requestPermissionsAsync()
@@ -30,33 +47,13 @@ export const useDownloadMedia = () => {
return { success: false, error: 'Permission denied' } return { success: false, error: 'Permission denied' }
} }
// 2. 生成临时文件名 setProgress(0.3)
const extension = type === 'video' ? 'mp4' : 'jpg'
const fileName = `download_${Date.now()}.${extension}`
fileUri = FileSystem.documentDirectory + fileName
// 3. 下载文件(带进度) // 2. 使用 MediaLibrary.saveToLibraryAsync 直接从 URL 保存
const downloadResumable = FileSystem.createDownloadResumable( // 这个方法可以直接接受远程 URL
url, await MediaLibrary.saveToLibraryAsync(url)
fileUri,
{},
(downloadProgress) => {
const progressValue = downloadProgress.totalBytesWritten / downloadProgress.totalBytesExpectedToWrite
setProgress(progressValue)
}
)
const downloadResult = await downloadResumable.downloadAsync()
if (!downloadResult?.uri) {
throw new Error('Download failed')
}
// 4. 保存到相册
await MediaLibrary.saveToLibraryAsync(downloadResult.uri)
// 5. 清理临时文件
await FileSystem.deleteAsync(fileUri, { idempotent: true })
setProgress(1)
setLoading(false) setLoading(false)
setProgress(0) setProgress(0)
return { success: true } return { success: true }
@@ -66,15 +63,6 @@ export const useDownloadMedia = () => {
setLoading(false) setLoading(false)
setProgress(0) setProgress(0)
// 尝试清理临时文件
if (fileUri) {
try {
await FileSystem.deleteAsync(fileUri, { idempotent: true })
} catch {
// 忽略清理错误
}
}
return { success: false, error: errorMessage } return { success: false, error: errorMessage }
} }
}, []) }, [])

View File

@@ -267,7 +267,7 @@ describe('useTabNavigation - core logic', () => {
describe('multi-language support', () => { describe('multi-language support', () => {
it('should generate tabs with Chinese names when language is zh-CN', () => { it('should generate tabs with Chinese names when language is zh-CN', () => {
// 模拟中文环境 // 模拟中文环境
const language = 'zh-CN' const language: string = 'zh-CN'
const tabs = mockCategoriesWithI18n.map(cat => const tabs = mockCategoriesWithI18n.map(cat =>
language === 'en-US' ? cat.nameEn : cat.name language === 'en-US' ? cat.nameEn : cat.name
) )

View File

@@ -3,6 +3,7 @@ import { useTemplateFavorite } from './use-template-favorite'
import { root } from '@repo/core' import { root } from '@repo/core'
import { TemplateSocialController } from '@repo/sdk' import { TemplateSocialController } from '@repo/sdk'
import { handleError } from './use-error' import { handleError } from './use-error'
import { templateSocialStore } from '@/stores/templateSocialStore'
jest.mock('@repo/core', () => ({ jest.mock('@repo/core', () => ({
root: { root: {
@@ -10,6 +11,15 @@ jest.mock('@repo/core', () => ({
}, },
})) }))
jest.mock('@/stores/templateSocialStore', () => ({
templateSocialStore: {
setFavorited: jest.fn(),
setFavoriteCount: jest.fn(),
getFavoriteCount: jest.fn(),
isFavorited: jest.fn().mockReturnValue(false),
},
}))
jest.mock('./use-error', () => ({ jest.mock('./use-error', () => ({
handleError: jest.fn(async (cb) => { handleError: jest.fn(async (cb) => {
try { try {
@@ -36,7 +46,6 @@ describe('useTemplateFavorite', () => {
it('should return initial state with no data', () => { it('should return initial state with no data', () => {
const { result } = renderHook(() => useTemplateFavorite()) const { result } = renderHook(() => useTemplateFavorite())
expect(result.current.favorited).toBe(false)
expect(result.current.loading).toBe(false) expect(result.current.loading).toBe(false)
expect(result.current.error).toBeNull() expect(result.current.error).toBeNull()
}) })
@@ -44,7 +53,6 @@ describe('useTemplateFavorite', () => {
it('should accept templateId parameter', () => { it('should accept templateId parameter', () => {
const { result } = renderHook(() => useTemplateFavorite(mockTemplateId)) const { result } = renderHook(() => useTemplateFavorite(mockTemplateId))
expect(result.current.favorited).toBe(false)
expect(result.current.loading).toBe(false) expect(result.current.loading).toBe(false)
expect(result.current.error).toBeNull() expect(result.current.error).toBeNull()
}) })
@@ -71,7 +79,7 @@ describe('useTemplateFavorite', () => {
expect(mockController.favorite).toHaveBeenCalledWith({ expect(mockController.favorite).toHaveBeenCalledWith({
templateId: mockTemplateId, templateId: mockTemplateId,
}) })
expect(result.current.favorited).toBe(true) expect(templateSocialStore.setFavorited).toHaveBeenCalledWith(mockTemplateId, true)
expect(result.current.loading).toBe(false) expect(result.current.loading).toBe(false)
expect(result.current.error).toBeNull() expect(result.current.error).toBeNull()
}) })
@@ -103,7 +111,8 @@ describe('useTemplateFavorite', () => {
}) })
expect(result.current.error).toEqual(mockError) expect(result.current.error).toEqual(mockError)
expect(result.current.favorited).toBe(false) // 错误时应该回滚乐观更新setFavorited 会被调用两次:先 true后 false
expect(templateSocialStore.setFavorited).toHaveBeenLastCalledWith(mockTemplateId, false)
expect(result.current.loading).toBe(false) expect(result.current.loading).toBe(false)
}) })
@@ -162,7 +171,7 @@ describe('useTemplateFavorite', () => {
expect(mockController.unfavorite).toHaveBeenCalledWith({ expect(mockController.unfavorite).toHaveBeenCalledWith({
templateId: mockTemplateId, templateId: mockTemplateId,
}) })
expect(result.current.favorited).toBe(false) expect(templateSocialStore.setFavorited).toHaveBeenCalledWith(mockTemplateId, false)
expect(result.current.loading).toBe(false) expect(result.current.loading).toBe(false)
expect(result.current.error).toBeNull() expect(result.current.error).toBeNull()
}) })
@@ -253,7 +262,7 @@ describe('useTemplateFavorite', () => {
expect(mockController.checkFavorited).toHaveBeenCalledWith({ expect(mockController.checkFavorited).toHaveBeenCalledWith({
templateId: mockTemplateId, templateId: mockTemplateId,
}) })
expect(result.current.favorited).toBe(true) expect(templateSocialStore.setFavorited).toHaveBeenCalledWith(mockTemplateId, true)
expect(result.current.loading).toBe(false) expect(result.current.loading).toBe(false)
expect(result.current.error).toBeNull() expect(result.current.error).toBeNull()
}) })
@@ -306,7 +315,7 @@ describe('useTemplateFavorite', () => {
await result.current.checkFavorited() await result.current.checkFavorited()
}) })
expect(result.current.favorited).toBe(false) expect(templateSocialStore.setFavorited).toHaveBeenCalledWith(mockTemplateId, false)
}) })
}) })

View File

@@ -3,6 +3,7 @@ import { useTemplateLike } from './use-template-like'
import { root } from '@repo/core' import { root } from '@repo/core'
import { TemplateSocialController } from '@repo/sdk' import { TemplateSocialController } from '@repo/sdk'
import { handleError } from './use-error' import { handleError } from './use-error'
import { templateSocialStore } from '@/stores/templateSocialStore'
jest.mock('@repo/core', () => ({ jest.mock('@repo/core', () => ({
root: { root: {
@@ -10,6 +11,15 @@ jest.mock('@repo/core', () => ({
}, },
})) }))
jest.mock('@/stores/templateSocialStore', () => ({
templateSocialStore: {
getLikeCount: jest.fn().mockReturnValue(0),
setLiked: jest.fn(),
setLikeCount: jest.fn(),
isLiked: jest.fn().mockReturnValue(false),
},
}))
jest.mock('./use-error', () => ({ jest.mock('./use-error', () => ({
handleError: jest.fn(async (cb) => { handleError: jest.fn(async (cb) => {
try { try {
@@ -34,7 +44,6 @@ describe('useTemplateLike', () => {
it('should return initial state with templateId', () => { it('should return initial state with templateId', () => {
const { result } = renderHook(() => useTemplateLike('template-1')) const { result } = renderHook(() => useTemplateLike('template-1'))
expect(result.current.liked).toBe(false)
expect(result.current.loading).toBe(false) expect(result.current.loading).toBe(false)
expect(result.current.error).toBeNull() expect(result.current.error).toBeNull()
}) })
@@ -42,7 +51,6 @@ describe('useTemplateLike', () => {
it('should return initial state without templateId', () => { it('should return initial state without templateId', () => {
const { result } = renderHook(() => useTemplateLike()) const { result } = renderHook(() => useTemplateLike())
expect(result.current.liked).toBe(false)
expect(result.current.loading).toBe(false) expect(result.current.loading).toBe(false)
expect(result.current.error).toBeNull() expect(result.current.error).toBeNull()
}) })
@@ -63,7 +71,7 @@ describe('useTemplateLike', () => {
}) })
expect(mockController.like).toHaveBeenCalledWith({ templateId: 'template-1' }) expect(mockController.like).toHaveBeenCalledWith({ templateId: 'template-1' })
expect(result.current.liked).toBe(true) expect(templateSocialStore.setLiked).toHaveBeenCalledWith('template-1', true)
expect(result.current.error).toBeNull() expect(result.current.error).toBeNull()
}) })
@@ -111,18 +119,13 @@ describe('useTemplateLike', () => {
const { result } = renderHook(() => useTemplateLike('template-1')) const { result } = renderHook(() => useTemplateLike('template-1'))
// 先设置为已点赞
act(() => {
result.current.like = jest.fn().mockResolvedValue({})
})
let response let response
await act(async () => { await act(async () => {
response = await result.current.unlike() response = await result.current.unlike()
}) })
expect(mockController.unlike).toHaveBeenCalledWith({ templateId: 'template-1' }) expect(mockController.unlike).toHaveBeenCalledWith({ templateId: 'template-1' })
expect(result.current.liked).toBe(false) expect(templateSocialStore.setLiked).toHaveBeenCalledWith('template-1', false)
expect(result.current.error).toBeNull() expect(result.current.error).toBeNull()
}) })
@@ -177,7 +180,7 @@ describe('useTemplateLike', () => {
}) })
expect(mockController.checkLiked).toHaveBeenCalledWith({ templateId: 'template-1' }) expect(mockController.checkLiked).toHaveBeenCalledWith({ templateId: 'template-1' })
expect(result.current.liked).toBe(true) expect(templateSocialStore.setLiked).toHaveBeenCalledWith('template-1', true)
expect(result.current.error).toBeNull() expect(result.current.error).toBeNull()
}) })
@@ -194,7 +197,7 @@ describe('useTemplateLike', () => {
await result.current.checkLiked() await result.current.checkLiked()
}) })
expect(result.current.liked).toBe(false) expect(templateSocialStore.setLiked).toHaveBeenCalledWith('template-1', false)
}) })
it('should handle API errors when checking liked status', async () => { it('should handle API errors when checking liked status', async () => {

View File

@@ -4,8 +4,16 @@
* 这个示例展示了如何使用 useUserFavorites Hook 来获取用户的收藏列表 * 这个示例展示了如何使用 useUserFavorites Hook 来获取用户的收藏列表
*/ */
import { useEffect, useState } from 'react'
import { FlatList, View, Text } from 'react-native'
import { useUserFavorites } from '@/hooks' import { useUserFavorites } from '@/hooks'
// 占位组件类型声明(实际项目中应从组件库导入)
declare const LoadingSpinner: React.FC
declare const LoadingMoreSpinner: React.FC
declare const ErrorMessage: React.FC<{ message: string }>
declare const FavoriteItem: React.FC<{ favorite: unknown }>
// 示例1基本用法 // 示例1基本用法
function MyFavoritesList() { function MyFavoritesList() {
const { const {
@@ -72,7 +80,7 @@ function SearchableFavorites() {
// 示例4下拉刷新 // 示例4下拉刷新
function RefreshableFavorites() { function RefreshableFavorites() {
const { favorites, loading, refreshing, refetch } = useUserFavorites() const { favorites, loading, refetch } = useUserFavorites()
const handleRefresh = () => { const handleRefresh = () => {
refetch() refetch()

View File

@@ -3,7 +3,6 @@ import {
type GetUserFavoritesInput, type GetUserFavoritesInput,
type GetUserFavoritesResponse, type GetUserFavoritesResponse,
TemplateSocialController, TemplateSocialController,
type UserFavoriteItem,
} from '@repo/sdk' } from '@repo/sdk'
import { useCallback, useRef, useState } from 'react' import { useCallback, useRef, useState } from 'react'
@@ -11,8 +10,10 @@ import { type ApiError } from '@/lib/types'
import { handleError } from './use-error' import { handleError } from './use-error'
/** 从 GetUserFavoritesResponse 中提取单个收藏项的类型 */
type UserFavoriteItem = GetUserFavoritesResponse['favorites'][number]
type GetUserFavoritesParams = Partial<Omit<GetUserFavoritesInput, 'page' | 'limit'>> type GetUserFavoritesParams = Partial<GetUserFavoritesInput>
const DEFAULT_PARAMS = { const DEFAULT_PARAMS = {
limit: 20, limit: 20,

View File

@@ -10,7 +10,7 @@ import { type ApiError } from '@/lib/types'
import { handleError } from './use-error' import { handleError } from './use-error'
type GetUserLikesParams = Partial<Omit<GetUserLikesInput, 'page'>> type GetUserLikesParams = Partial<GetUserLikesInput>
const DEFAULT_PARAMS = { const DEFAULT_PARAMS = {
limit: 20, limit: 20,