Files
expo-popcore-app/app/(tabs)/video.test.tsx
imeepos e0a56710d0 feat: integrate UI components into Video tab
Replace inline LoadingState, ErrorState, and FooterLoading components with reusable UI components (LoadingState, ErrorState, RefreshControl, PaginationLoader). Add retry functionality to ErrorState and maintain consistent styling with #FFE500 color theme.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 12:06:46 +08:00

355 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/RefreshControl', () => 'RefreshControl')
jest.mock('@/components/PaginationLoader', () => 'PaginationLoader')
// 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)
})
})
})
})