diff --git a/app/(tabs)/video.test.tsx b/app/(tabs)/video.test.tsx index 0bb72a4..f5d3861 100644 --- a/app/(tabs)/video.test.tsx +++ b/app/(tabs)/video.test.tsx @@ -46,6 +46,106 @@ jest.mock('@/components/LoadingState', () => 'LoadingState') jest.mock('@/components/ErrorState', () => 'ErrorState') jest.mock('@/components/PaginationLoader', () => 'PaginationLoader') +// Mock VideoSocialButton +jest.mock('@/components/blocks/ui/VideoSocialButton', () => ({ + VideoSocialButton: ({ testID, liked, favorited, likeCount, favoriteCount }: any) => { + const React = require('react') + const { View, Text } = require('react-native') + return React.createElement( + View, + { testID }, + liked !== undefined && React.createElement(Text, { testID: `${testID}-like-button` }, liked ? 'liked' : 'not liked'), + favorited !== undefined && React.createElement(Text, { testID: `${testID}-favorite-button` }, favorited ? 'favorited' : 'not favorited'), + likeCount !== undefined && React.createElement(Text, {}, likeCount.toString()), + favoriteCount !== undefined && React.createElement(Text, {}, favoriteCount.toString()) + ) + }, +})) + +// Mock LikeButton and FavoriteButton +jest.mock('@/components/blocks/ui/LikeButton', () => ({ + LikeButton: ({ testID, count }: any) => { + const React = require('react') + const { Text } = require('react-native') + return React.createElement(Text, { testID }, count !== undefined ? count.toString() : 'LikeButton') + }, +})) + +jest.mock('@/components/blocks/ui/FavoriteButton', () => ({ + FavoriteButton: ({ testID, count }: any) => { + const React = require('react') + const { Text } = require('react-native') + return React.createElement(Text, { testID }, count !== undefined ? count.toString() : 'FavoriteButton') + }, +})) + +// Mock hooks +jest.mock('@/hooks/use-template-like', () => ({ + useTemplateLike: jest.fn(() => ({ + liked: false, + loading: false, + like: jest.fn(), + unlike: jest.fn(), + checkLiked: jest.fn(), + })), +})) + +jest.mock('@/hooks/use-template-favorite', () => ({ + useTemplateFavorite: jest.fn(() => ({ + favorited: false, + loading: false, + favorite: jest.fn(), + unfavorite: jest.fn(), + checkFavorited: jest.fn(), + })), +})) + +jest.mock('@/stores/templateSocialStore', () => { + const actualStore = jest.requireActual('@/stores/templateSocialStore') + return { + ...actualStore, + useTemplateSocialStore: jest.fn((selector) => { + const state = { + likedMap: {}, + favoritedMap: {}, + likeCountMap: {}, + favoriteCountMap: {}, + setLikedStates: jest.fn(), + setFavoritedStates: jest.fn(), + setLikeCountStates: jest.fn(), + setFavoriteCountStates: jest.fn(), + setLiked: jest.fn(), + setFavorited: jest.fn(), + setLikeCount: jest.fn(), + setFavoriteCount: jest.fn(), + incrementLikeCount: jest.fn(), + decrementLikeCount: jest.fn(), + getLiked: jest.fn(), + getFavorited: jest.fn(), + getLikeCount: jest.fn(), + getFavoriteCount: jest.fn(), + isLiked: jest.fn(() => false), + isFavorited: jest.fn(() => false), + clear: jest.fn(), + } + if (typeof selector === 'function') { + return selector(state) + } + return state + }), + templateSocialStore: { + useLiked: jest.fn(() => false), + useFavorited: jest.fn(() => false), + useLikeCount: jest.fn(() => undefined), + useFavoriteCount: jest.fn(() => undefined), + }, + useTemplateLiked: jest.fn(() => false), + useTemplateFavorited: jest.fn(() => false), + useTemplateLikeCount: jest.fn(() => undefined), + useTemplateFavoriteCount: jest.fn(() => undefined), + } +}) + // Mock react-native RefreshControl (directly from react-native) jest.mock('react-native', () => Object.assign({}, jest.requireActual('react-native'), { @@ -194,6 +294,61 @@ describe('Video Screen', () => { }) }) + describe('VideoItem 社交按钮集成', () => { + const mockItem = createMockTemplateDetail() as TemplateDetail + const mockVideoHeight = 600 + + it('应该渲染社交按钮', () => { + const { getByTestId } = render( + + ) + + expect(getByTestId('video-social-button')).toBeTruthy() + }) + + it('应该显示点赞数量', () => { + const itemWithLikes = createMockTemplateDetail({ + likeCount: 150, + }) as TemplateDetail + + const { getByText } = render( + + ) + + expect(getByText('150')).toBeTruthy() + }) + + it('应该显示收藏数量', () => { + const itemWithFavorites = createMockTemplateDetail({ + favoriteCount: 80, + }) as TemplateDetail + + const { getByText } = render( + + ) + + expect(getByText('80')).toBeTruthy() + }) + + it('应该有点赞交互功能', () => { + const { getByTestId } = render( + + ) + + const likeButton = getByTestId('video-social-button-like-button') + expect(likeButton).toBeTruthy() + }) + + it('应该有收藏交互功能', () => { + const { getByTestId } = render( + + ) + + const favoriteButton = getByTestId('video-social-button-favorite-button') + expect(favoriteButton).toBeTruthy() + }) + }) + describe('VideoScreen Component', () => { it('should show loading state when templates are loading', () => { mockUseTemplates.mockReturnValue({ diff --git a/app/(tabs)/video.tsx b/app/(tabs)/video.tsx index b2e6ef3..3965920 100644 --- a/app/(tabs)/video.tsx +++ b/app/(tabs)/video.tsx @@ -17,6 +17,16 @@ import { useTranslation } from 'react-i18next' import { SameStyleIcon, WhiteStarIcon } from '@/components/icon' import { useRouter } from 'expo-router' import { useTemplates, type TemplateDetail } from '@/hooks' +import { VideoSocialButton } from '@/components/blocks/ui/VideoSocialButton' +import { useTemplateLike } from '@/hooks/use-template-like' +import { useTemplateFavorite } from '@/hooks/use-template-favorite' +import { + useTemplateSocialStore, + useTemplateLiked, + useTemplateFavorited, + useTemplateLikeCount, + useTemplateFavoriteCount, +} from '@/stores/templateSocialStore' import LoadingState from '@/components/LoadingState' import ErrorState from '@/components/ErrorState' import PaginationLoader from '@/components/PaginationLoader' @@ -67,6 +77,16 @@ export const VideoItem = memo(({ item, videoHeight }: { item: TemplateDetail; vi const router = useRouter() const [imageSize, setImageSize] = useState<{ width: number; height: number } | null>(null) + // 从 store 获取状态 + const liked = useTemplateLiked(item.id) ?? false + const favorited = useTemplateFavorited(item.id) ?? false + const likeCount = useTemplateLikeCount(item.id) ?? item.likeCount + const favoriteCount = useTemplateFavoriteCount(item.id) ?? item.favoriteCount + + // 使用 hooks 获取操作函数 + const { like: onLike, unlike: onUnlike, loading: likeLoading } = useTemplateLike(item.id) + const { favorite: onFavorite, unfavorite: onUnfavorite, loading: favoriteLoading } = useTemplateFavorite(item.id) + const handleImageLoad = useCallback((event: ImageLoadEventData) => { const { source } = event if (source?.width && source?.height) { @@ -109,6 +129,20 @@ export const VideoItem = memo(({ item, videoHeight }: { item: TemplateDetail; vi {t('video.makeSame')} + {/* 社交按钮 */} + ) }) diff --git a/stores/templateSocialStore.ts b/stores/templateSocialStore.ts index 1751a82..abc87f4 100644 --- a/stores/templateSocialStore.ts +++ b/stores/templateSocialStore.ts @@ -11,6 +11,9 @@ interface TemplateSocialState { // 点赞数量缓存: templateId -> number likeCountMap: Record + // 收藏数量缓存: templateId -> number + favoriteCountMap: Record + // 批量更新点赞状态 setLikedStates: (states: Record) => void @@ -20,6 +23,9 @@ interface TemplateSocialState { // 批量更新点赞数量 setLikeCountStates: (states: Record) => void + // 批量更新收藏数量 + setFavoriteCountStates: (states: Record) => void + // 更新单个点赞状态 setLiked: (templateId: string, liked: boolean) => void @@ -29,6 +35,9 @@ interface TemplateSocialState { // 更新单个点赞数量 setLikeCount: (templateId: string, count: number) => void + // 更新单个收藏数量 + setFavoriteCount: (templateId: string, count: number) => void + // 增加点赞数量(乐观更新) incrementLikeCount: (templateId: string) => void @@ -44,6 +53,9 @@ interface TemplateSocialState { // 获取点赞数量(如果不存在返回 undefined) getLikeCount: (templateId: string) => number | undefined + // 获取收藏数量(如果不存在返回 undefined) + getFavoriteCount: (templateId: string) => number | undefined + // 获取点赞状态(如果不存在返回 false) isLiked: (templateId: string) => boolean @@ -61,6 +73,7 @@ export const useTemplateSocialStore = create()( likedMap: {}, favoritedMap: {}, likeCountMap: {}, + favoriteCountMap: {}, // Actions setLikedStates: (states) => { @@ -81,6 +94,12 @@ export const useTemplateSocialStore = create()( })) }, + setFavoriteCountStates: (states) => { + set((state) => ({ + favoriteCountMap: { ...state.favoriteCountMap, ...states }, + })) + }, + setLiked: (templateId, liked) => { set((state) => ({ likedMap: { ...state.likedMap, [templateId]: liked }, @@ -99,6 +118,12 @@ export const useTemplateSocialStore = create()( })) }, + setFavoriteCount: (templateId, count) => { + set((state) => ({ + favoriteCountMap: { ...state.favoriteCountMap, [templateId]: count }, + })) + }, + incrementLikeCount: (templateId) => { set((state) => ({ likeCountMap: { @@ -132,6 +157,10 @@ export const useTemplateSocialStore = create()( return get().likeCountMap[templateId] }, + getFavoriteCount: (templateId) => { + return get().favoriteCountMap[templateId] + }, + isLiked: (templateId) => { return get().likedMap[templateId] || false }, @@ -145,6 +174,7 @@ export const useTemplateSocialStore = create()( likedMap: {}, favoritedMap: {}, likeCountMap: {}, + favoriteCountMap: {}, }) }, }), @@ -171,3 +201,12 @@ export function useTemplateLikeCount(templateId: string | undefined) { templateId ? state.likeCountMap[templateId] : undefined ) } + +export function useTemplateFavoriteCount(templateId: string | undefined) { + return useTemplateSocialStore((state) => + templateId ? state.favoriteCountMap[templateId] : undefined + ) +} + +// 为了方便使用,导出 templateSocialStore 别名 +export const templateSocialStore = useTemplateSocialStore