expo-popcore-app/app/(tabs)/video.test.tsx

361 lines
10 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 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 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('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('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)
})
})
})
})