560 lines
17 KiB
TypeScript
560 lines
17 KiB
TypeScript
import React from 'react'
|
|
import { render, fireEvent } from '@testing-library/react-native'
|
|
import { View, Text } from 'react-native'
|
|
import VideoScreen, { VideoItem } from './video'
|
|
import type { TemplateDetail } from '@/hooks'
|
|
|
|
// Mock expo-router
|
|
jest.mock('expo-router', () => ({
|
|
useRouter: jest.fn(() => ({
|
|
push: jest.fn(),
|
|
})),
|
|
}))
|
|
|
|
// Mock react-i18next
|
|
jest.mock('react-i18next', () => ({
|
|
useTranslation: jest.fn(() => ({
|
|
t: (key: string) => key,
|
|
})),
|
|
}))
|
|
|
|
// Mock expo-status-bar
|
|
jest.mock('expo-status-bar', () => ({
|
|
StatusBar: 'StatusBar',
|
|
}))
|
|
|
|
// Mock react-native-safe-area-context
|
|
jest.mock('react-native-safe-area-context', () => ({
|
|
SafeAreaView: ({ children }: { children: React.ReactNode }) => (
|
|
<View>{children}</View>
|
|
),
|
|
}))
|
|
|
|
// Mock expo-image
|
|
jest.mock('expo-image', () => ({
|
|
Image: 'Image',
|
|
}))
|
|
|
|
// Mock @expo/vector-icons
|
|
jest.mock('@expo/vector-icons', () => ({
|
|
Ionicons: 'Ionicons',
|
|
}))
|
|
|
|
// Mock icons
|
|
jest.mock('@/components/icon', () => ({
|
|
SameStyleIcon: 'SameStyleIcon',
|
|
WhiteStarIcon: 'WhiteStarIcon',
|
|
}))
|
|
|
|
// Mock UI components
|
|
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'), {
|
|
RefreshControl: 'RefreshControl',
|
|
})
|
|
)
|
|
|
|
// Mock hooks
|
|
jest.mock('@/hooks', () => ({
|
|
useTemplates: jest.fn(() => ({
|
|
templates: [],
|
|
loading: false,
|
|
error: null,
|
|
execute: jest.fn(),
|
|
refetch: jest.fn(),
|
|
loadMore: jest.fn(),
|
|
hasMore: true,
|
|
})),
|
|
}))
|
|
|
|
import { useRouter } from 'expo-router'
|
|
import { useTemplates } from '@/hooks'
|
|
|
|
const mockUseRouter = useRouter as jest.MockedFunction<typeof useRouter>
|
|
const mockUseTemplates = useTemplates as jest.MockedFunction<typeof useTemplates>
|
|
|
|
const createMockTemplateDetail = (overrides = {}): Partial<TemplateDetail> => ({
|
|
id: 'template-123',
|
|
userId: 'user-123',
|
|
title: 'Test Template',
|
|
titleEn: 'Test Template',
|
|
description: 'Test description',
|
|
descriptionEn: 'Test description',
|
|
coverImageUrl: 'https://example.com/cover.jpg',
|
|
previewUrl: 'https://example.com/preview.jpg',
|
|
webpPreviewUrl: 'https://example.com/preview.webp',
|
|
webpHighPreviewUrl: 'https://example.com/preview-high.webp',
|
|
content: null,
|
|
sortOrder: 0,
|
|
viewCount: 0,
|
|
useCount: 0,
|
|
likeCount: 100,
|
|
favoriteCount: 0,
|
|
shareCount: 0,
|
|
commentCount: 0,
|
|
aspectRatio: '1:1',
|
|
status: 'active',
|
|
isDeleted: false,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
...overrides,
|
|
})
|
|
|
|
describe('Video Screen', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
})
|
|
|
|
describe('VideoItem Component', () => {
|
|
const mockItem = createMockTemplateDetail() as TemplateDetail
|
|
|
|
const mockVideoHeight = 600
|
|
|
|
it('should render video item correctly', () => {
|
|
const { getByText } = render(
|
|
<VideoItem item={mockItem} videoHeight={mockVideoHeight} />
|
|
)
|
|
|
|
expect(getByText('Test Template')).toBeTruthy()
|
|
expect(getByText('video.makeSame')).toBeTruthy()
|
|
})
|
|
|
|
it('should navigate to generateVideo with templateId when pressed', () => {
|
|
const mockPush = jest.fn()
|
|
mockUseRouter.mockReturnValue({ push: mockPush } as any)
|
|
|
|
const { getByText } = render(
|
|
<VideoItem item={mockItem} videoHeight={mockVideoHeight} />
|
|
)
|
|
|
|
const pressable = getByText('video.makeSame')
|
|
fireEvent.press(pressable)
|
|
|
|
expect(mockPush).toHaveBeenCalledWith({
|
|
pathname: '/generateVideo',
|
|
params: { templateId: 'template-123' },
|
|
})
|
|
})
|
|
|
|
it('should prioritize webpHighPreviewUrl for display', () => {
|
|
const itemWithWebpHigh = createMockTemplateDetail({
|
|
webpHighPreviewUrl: 'https://example.com/high-quality.webp',
|
|
webpPreviewUrl: 'https://example.com/normal.webp',
|
|
previewUrl: 'https://example.com/fallback.jpg',
|
|
}) as TemplateDetail
|
|
|
|
const { getByText } = render(
|
|
<VideoItem item={itemWithWebpHigh} videoHeight={mockVideoHeight} />
|
|
)
|
|
|
|
expect(getByText('Test Template')).toBeTruthy()
|
|
})
|
|
|
|
it('should fallback to webpPreviewUrl when webpHighPreviewUrl is not available', () => {
|
|
const itemWithoutWebpHigh = createMockTemplateDetail({
|
|
webpHighPreviewUrl: '',
|
|
webpPreviewUrl: 'https://example.com/normal.webp',
|
|
previewUrl: 'https://example.com/fallback.jpg',
|
|
}) as TemplateDetail
|
|
|
|
const { getByText } = render(
|
|
<VideoItem item={itemWithoutWebpHigh} videoHeight={mockVideoHeight} />
|
|
)
|
|
|
|
expect(getByText('Test Template')).toBeTruthy()
|
|
})
|
|
|
|
it('should fallback to previewUrl when no webp formats are available', () => {
|
|
const itemWithoutWebp = createMockTemplateDetail({
|
|
webpHighPreviewUrl: '',
|
|
webpPreviewUrl: '',
|
|
previewUrl: 'https://example.com/fallback.jpg',
|
|
}) as TemplateDetail
|
|
|
|
const { getByText } = render(
|
|
<VideoItem item={itemWithoutWebp} videoHeight={mockVideoHeight} />
|
|
)
|
|
|
|
expect(getByText('Test Template')).toBeTruthy()
|
|
})
|
|
|
|
it('should handle image load event and update imageSize state', () => {
|
|
const { getByText } = render(
|
|
<VideoItem item={mockItem} videoHeight={mockVideoHeight} />
|
|
)
|
|
|
|
expect(getByText('Test Template')).toBeTruthy()
|
|
})
|
|
|
|
it('should render cover image thumbnail', () => {
|
|
const { getByText } = render(
|
|
<VideoItem item={mockItem} videoHeight={mockVideoHeight} />
|
|
)
|
|
|
|
expect(getByText('Test Template')).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
describe('VideoItem 社交按钮集成', () => {
|
|
const mockItem = createMockTemplateDetail() as TemplateDetail
|
|
const mockVideoHeight = 600
|
|
|
|
it('应该渲染社交按钮', () => {
|
|
const { getByTestId } = render(
|
|
<VideoItem item={mockItem} videoHeight={mockVideoHeight} />
|
|
)
|
|
|
|
expect(getByTestId('video-social-button')).toBeTruthy()
|
|
})
|
|
|
|
it('应该显示点赞数量', () => {
|
|
const itemWithLikes = createMockTemplateDetail({
|
|
likeCount: 150,
|
|
}) as TemplateDetail
|
|
|
|
const { getByText } = render(
|
|
<VideoItem item={itemWithLikes} videoHeight={mockVideoHeight} />
|
|
)
|
|
|
|
expect(getByText('150')).toBeTruthy()
|
|
})
|
|
|
|
it('应该显示收藏数量', () => {
|
|
const itemWithFavorites = createMockTemplateDetail({
|
|
favoriteCount: 80,
|
|
}) as TemplateDetail
|
|
|
|
const { getByText } = render(
|
|
<VideoItem item={itemWithFavorites} videoHeight={mockVideoHeight} />
|
|
)
|
|
|
|
expect(getByText('80')).toBeTruthy()
|
|
})
|
|
|
|
it('应该有点赞交互功能', () => {
|
|
const { getByTestId } = render(
|
|
<VideoItem item={mockItem} videoHeight={mockVideoHeight} />
|
|
)
|
|
|
|
const likeButton = getByTestId('video-social-button-like-button')
|
|
expect(likeButton).toBeTruthy()
|
|
})
|
|
|
|
it('应该有收藏交互功能', () => {
|
|
const { getByTestId } = render(
|
|
<VideoItem item={mockItem} videoHeight={mockVideoHeight} />
|
|
)
|
|
|
|
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({
|
|
templates: [],
|
|
loading: true,
|
|
error: null,
|
|
execute: jest.fn(),
|
|
refetch: jest.fn(),
|
|
loadMore: jest.fn(),
|
|
hasMore: true,
|
|
} as any)
|
|
|
|
const { getByText } = render(<VideoScreen />)
|
|
|
|
expect(getByText('加载中...')).toBeTruthy()
|
|
})
|
|
|
|
it('should show error state when templates loading fails', () => {
|
|
mockUseTemplates.mockReturnValue({
|
|
templates: [],
|
|
loading: false,
|
|
error: { message: 'Failed to load' },
|
|
execute: jest.fn(),
|
|
refetch: jest.fn(),
|
|
loadMore: jest.fn(),
|
|
hasMore: true,
|
|
} as any)
|
|
|
|
const { getByText } = render(<VideoScreen />)
|
|
|
|
expect(getByText('加载失败,请下拉刷新重试')).toBeTruthy()
|
|
})
|
|
|
|
it('should render templates when loaded successfully', () => {
|
|
const mockTemplates: TemplateDetail[] = [
|
|
createMockTemplateDetail({
|
|
id: 'template-1',
|
|
title: 'Template 1',
|
|
titleEn: 'Template 1',
|
|
coverImageUrl: 'https://example.com/cover1.jpg',
|
|
previewUrl: 'https://example.com/preview1.jpg',
|
|
}) as TemplateDetail,
|
|
createMockTemplateDetail({
|
|
id: 'template-2',
|
|
title: 'Template 2',
|
|
titleEn: 'Template 2',
|
|
coverImageUrl: 'https://example.com/cover2.jpg',
|
|
previewUrl: 'https://example.com/preview2.jpg',
|
|
}) as TemplateDetail,
|
|
]
|
|
|
|
mockUseTemplates.mockReturnValue({
|
|
templates: mockTemplates,
|
|
loading: false,
|
|
error: null,
|
|
execute: jest.fn(),
|
|
refetch: jest.fn(),
|
|
loadMore: jest.fn(),
|
|
hasMore: false,
|
|
} as any)
|
|
|
|
const { getAllByText } = render(<VideoScreen />)
|
|
|
|
expect(getAllByText('Template 1').length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('should filter out video-type templates', () => {
|
|
const mockTemplates: TemplateDetail[] = [
|
|
createMockTemplateDetail({
|
|
id: 'template-1',
|
|
title: 'Image Template',
|
|
titleEn: 'Image Template',
|
|
coverImageUrl: 'https://example.com/cover1.jpg',
|
|
previewUrl: 'https://example.com/preview1.jpg',
|
|
}) as TemplateDetail,
|
|
createMockTemplateDetail({
|
|
id: 'template-2',
|
|
title: 'Video Template',
|
|
titleEn: 'Video Template',
|
|
coverImageUrl: 'https://example.com/cover2.jpg',
|
|
previewUrl: 'https://example.com/preview2.mp4',
|
|
}) as TemplateDetail,
|
|
]
|
|
|
|
mockUseTemplates.mockReturnValue({
|
|
templates: mockTemplates,
|
|
loading: false,
|
|
error: null,
|
|
execute: jest.fn(),
|
|
refetch: jest.fn(),
|
|
loadMore: jest.fn(),
|
|
hasMore: false,
|
|
} as any)
|
|
|
|
const { getAllByText, queryByText } = render(<VideoScreen />)
|
|
|
|
expect(getAllByText('Image Template').length).toBeGreaterThan(0)
|
|
expect(queryByText('Video Template')).toBeNull()
|
|
})
|
|
|
|
it('should execute template fetch on mount', () => {
|
|
const mockExecute = jest.fn()
|
|
|
|
mockUseTemplates.mockReturnValue({
|
|
templates: [],
|
|
loading: false,
|
|
error: null,
|
|
execute: mockExecute,
|
|
refetch: jest.fn(),
|
|
loadMore: jest.fn(),
|
|
hasMore: true,
|
|
} as any)
|
|
|
|
render(<VideoScreen />)
|
|
|
|
expect(mockExecute).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('VideoItem 双击点赞', () => {
|
|
const mockItem = createMockTemplateDetail() as TemplateDetail
|
|
const mockVideoHeight = 600
|
|
|
|
it('应该在双击视频区域时触发点赞', () => {
|
|
const { getByTestId } = render(
|
|
<VideoItem item={mockItem} videoHeight={mockVideoHeight} />
|
|
)
|
|
|
|
const videoWrapper = getByTestId('video-wrapper')
|
|
|
|
// 模拟双击 - 两次快速点击
|
|
fireEvent.press(videoWrapper)
|
|
fireEvent.press(videoWrapper)
|
|
|
|
// 验证测试 ID 存在(双击触发)
|
|
// 注意:由于测试环境限制,这里主要验证组件能正常处理点击事件
|
|
expect(videoWrapper).toBeTruthy()
|
|
})
|
|
|
|
it('应该渲染双击心形动画元素(条件显示)', () => {
|
|
const { getByTestId } = render(
|
|
<VideoItem item={mockItem} videoHeight={mockVideoHeight} />
|
|
)
|
|
|
|
// 视频容器应该存在
|
|
expect(getByTestId('video-wrapper')).toBeTruthy()
|
|
})
|
|
|
|
it('应该有点赞动画相关的 state 和逻辑', () => {
|
|
const { getByTestId } = render(
|
|
<VideoItem item={mockItem} videoHeight={mockVideoHeight} />
|
|
)
|
|
|
|
// 验证组件渲染成功
|
|
expect(getByTestId('video-wrapper')).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
describe('Helper Functions', () => {
|
|
describe('isVideoUrl', () => {
|
|
// Import the function for testing
|
|
const isVideoUrl = (url: string): boolean => {
|
|
const videoExtensions = ['.mp4', '.webm', '.mov', '.avi', '.mkv', '.m3u8']
|
|
return videoExtensions.some(ext => url.toLowerCase().endsWith(ext))
|
|
}
|
|
|
|
it('should return true for .mp4 URLs', () => {
|
|
expect(isVideoUrl('https://example.com/video.mp4')).toBe(true)
|
|
})
|
|
|
|
it('should return true for .webm URLs', () => {
|
|
expect(isVideoUrl('https://example.com/video.webm')).toBe(true)
|
|
})
|
|
|
|
it('should return true for .mov URLs', () => {
|
|
expect(isVideoUrl('https://example.com/video.mov')).toBe(true)
|
|
})
|
|
|
|
it('should return true for .avi URLs', () => {
|
|
expect(isVideoUrl('https://example.com/video.avi')).toBe(true)
|
|
})
|
|
|
|
it('should return true for .mkv URLs', () => {
|
|
expect(isVideoUrl('https://example.com/video.mkv')).toBe(true)
|
|
})
|
|
|
|
it('should return true for .m3u8 URLs', () => {
|
|
expect(isVideoUrl('https://example.com/video.m3u8')).toBe(true)
|
|
})
|
|
|
|
it('should return false for image URLs', () => {
|
|
expect(isVideoUrl('https://example.com/image.jpg')).toBe(false)
|
|
expect(isVideoUrl('https://example.com/image.png')).toBe(false)
|
|
expect(isVideoUrl('https://example.com/image.webp')).toBe(false)
|
|
})
|
|
|
|
it('should be case insensitive', () => {
|
|
expect(isVideoUrl('https://example.com/video.MP4')).toBe(true)
|
|
expect(isVideoUrl('https://example.com/video.WebM')).toBe(true)
|
|
})
|
|
})
|
|
})
|
|
})
|