feat: 重构测试结构并添加动态表单组件
主要更改: - 重构测试文件结构,删除旧的测试文件并添加新的测试覆盖 - 添加 DynamicForm 组件及其测试 - 更新 Jest 配置以支持新的测试结构 - 更新组件、抽屉和国际化文件 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,913 +0,0 @@
|
||||
/**
|
||||
* Comprehensive tests for app/(tabs)/index.tsx
|
||||
*
|
||||
* Test coverage focuses on:
|
||||
* 1. Card Component - Aspect Ratio Parsing
|
||||
* 2. Card Component - Rendering
|
||||
* 3. Category Selection Logic
|
||||
* 4. URL Params Handling - categoryId
|
||||
* 5. Navigation Actions
|
||||
* 6. Template Filtering
|
||||
* 7. Loading States
|
||||
* 8. Empty States
|
||||
* 9. Sticky Tabs Behavior
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { render, waitFor } from '@testing-library/react-native'
|
||||
|
||||
import HomeScreen from './index'
|
||||
import { useCategories } from '@/hooks/use-categories'
|
||||
import { useActivates } from '@/hooks/use-activates'
|
||||
import { useCategoriesStore } from '@/hooks/use-categories'
|
||||
import type { Category, CategoryTemplate } from '@repo/sdk'
|
||||
|
||||
// Helper function to create mock category
|
||||
const createMockCategory = (id: string, name: string, templates: CategoryTemplate[] = []): Category => ({
|
||||
id,
|
||||
name,
|
||||
nameEn: name,
|
||||
description: '',
|
||||
descriptionEn: '',
|
||||
sortOrder: 0,
|
||||
isActive: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
templates,
|
||||
})
|
||||
|
||||
// Helper function to create mock template
|
||||
const createMockTemplate = (
|
||||
id: string,
|
||||
title: string,
|
||||
options: {
|
||||
previewUrl?: string
|
||||
webpPreviewUrl?: string
|
||||
aspectRatio?: string | number
|
||||
} = {}
|
||||
): CategoryTemplate & { webpPreviewUrl?: string } => ({
|
||||
id,
|
||||
title,
|
||||
tag: {
|
||||
id: 'tag1',
|
||||
name: 'Tag 1',
|
||||
nameEn: 'Tag 1',
|
||||
categoryTagId: 'catTag1',
|
||||
sortOrder: 0,
|
||||
},
|
||||
length: 10,
|
||||
previewUrl: options.previewUrl || `https://example.com/${id}.jpg`,
|
||||
coverImageUrl: options.previewUrl || `https://example.com/${id}.jpg`,
|
||||
aspectRatio: options.aspectRatio ? String(options.aspectRatio) : '16:9',
|
||||
webpPreviewUrl: options.webpPreviewUrl || `https://example.com/${id}.webp`,
|
||||
})
|
||||
|
||||
// Mock all dependencies
|
||||
jest.mock('expo-image', () => ({
|
||||
Image: 'Image',
|
||||
}))
|
||||
|
||||
jest.mock('expo-linear-gradient', () => ({
|
||||
LinearGradient: 'LinearGradient',
|
||||
}))
|
||||
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'home.tabs.featured': 'Featured',
|
||||
'home.tabs.christmas': 'Christmas',
|
||||
'home.tabs.pets': 'Pets',
|
||||
'home.tabs.avatar': 'Avatar',
|
||||
'home.tabs.theater1': 'Theater 1',
|
||||
'home.tabs.theater2': 'Theater 2',
|
||||
}
|
||||
return translations[key] || key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
jest.mock('@/hooks/use-categories')
|
||||
jest.mock('@/hooks/use-activates')
|
||||
|
||||
jest.mock('react-native-safe-area-context', () => ({
|
||||
SafeAreaView: ({ children }: { children: React.ReactNode }) => React.createElement('View', null, children),
|
||||
useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 }),
|
||||
}))
|
||||
|
||||
describe('HomeScreen', () => {
|
||||
const mockUseCategories = useCategories as jest.MockedFunction<typeof useCategories>
|
||||
const mockUseActivates = useActivates as jest.MockedFunction<typeof useActivates>
|
||||
const mockPush = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
useCategoriesStore.setState({
|
||||
data: undefined,
|
||||
loading: false,
|
||||
error: null,
|
||||
hasLoaded: false,
|
||||
})
|
||||
|
||||
// Reset router mock
|
||||
jest.doMock('expo-router', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
}),
|
||||
useLocalSearchParams: () => ({}),
|
||||
}))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('CRITICAL: Card Component - Aspect Ratio Parsing', () => {
|
||||
it('should parse aspect ratio string format "128:128" correctly to 1', async () => {
|
||||
const mockCategoriesData = {
|
||||
categories: [
|
||||
createMockCategory('cat1', 'Category 1', [
|
||||
createMockTemplate('tpl1', 'Template 1', {
|
||||
aspectRatio: '128:128',
|
||||
previewUrl: 'https://example.com/image.jpg',
|
||||
webpPreviewUrl: 'https://example.com/image.webp',
|
||||
}),
|
||||
]),
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 1000,
|
||||
}
|
||||
|
||||
mockUseCategories.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: mockCategoriesData,
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
mockUseActivates.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: { activities: [], total: 0, page: 1, limit: 10, totalPages: 0 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const { getByText } = render(<HomeScreen />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Template 1')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
it('should parse aspect ratio string format "16:9" correctly to 1.777', async () => {
|
||||
const mockCategoriesData = {
|
||||
categories: [
|
||||
createMockCategory('cat1', 'Category 1', [
|
||||
createMockTemplate('tpl1', 'Wide Template', {
|
||||
aspectRatio: '16:9',
|
||||
previewUrl: 'https://example.com/wide.jpg',
|
||||
webpPreviewUrl: 'https://example.com/wide.webp',
|
||||
}),
|
||||
]),
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 1000,
|
||||
}
|
||||
|
||||
mockUseCategories.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: mockCategoriesData,
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
mockUseActivates.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: { activities: [], total: 0, page: 1, limit: 10, totalPages: 0 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const { getByText } = render(<HomeScreen />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Wide Template')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle numeric aspect ratio directly without parsing', async () => {
|
||||
const mockCategoriesData = {
|
||||
categories: [
|
||||
createMockCategory('cat1', 'Category 1', [
|
||||
createMockTemplate('tpl1', 'Numeric Ratio Template', {
|
||||
aspectRatio: 1.5,
|
||||
previewUrl: 'https://example.com/image.jpg',
|
||||
webpPreviewUrl: 'https://example.com/image.webp',
|
||||
}),
|
||||
]),
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 1000,
|
||||
}
|
||||
|
||||
mockUseCategories.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: mockCategoriesData,
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
mockUseActivates.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: { activities: [], total: 0, page: 1, limit: 10, totalPages: 0 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const { getByText } = render(<HomeScreen />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Numeric Ratio Template')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle missing aspectRatio gracefully', async () => {
|
||||
const template = createMockTemplate('tpl1', 'No Ratio Template', {
|
||||
previewUrl: 'https://example.com/image.jpg',
|
||||
webpPreviewUrl: 'https://example.com/image.webp',
|
||||
})
|
||||
// @ts-ignore - testing missing aspectRatio
|
||||
delete template.aspectRatio
|
||||
|
||||
const mockCategoriesData = {
|
||||
categories: [
|
||||
createMockCategory('cat1', 'Category 1', [template]),
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 1000,
|
||||
}
|
||||
|
||||
mockUseCategories.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: mockCategoriesData,
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
mockUseActivates.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: { activities: [], total: 0, page: 1, limit: 10, totalPages: 0 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const { getByText } = render(<HomeScreen />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('No Ratio Template')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('CRITICAL: Card Component - Rendering', () => {
|
||||
it('should render card with correct image source', async () => {
|
||||
const mockCategoriesData = {
|
||||
categories: [
|
||||
createMockCategory('cat1', 'Category 1', [
|
||||
createMockTemplate('tpl1', 'Test Template', {
|
||||
previewUrl: 'https://example.com/test.jpg',
|
||||
webpPreviewUrl: 'https://example.com/test.webp',
|
||||
}),
|
||||
]),
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 1000,
|
||||
}
|
||||
|
||||
mockUseCategories.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: mockCategoriesData,
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
mockUseActivates.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: { activities: [], total: 0, page: 1, limit: 10, totalPages: 0 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const { getByText } = render(<HomeScreen />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Test Template')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
it('should render card title with numberOfLines={1}', async () => {
|
||||
const mockCategoriesData = {
|
||||
categories: [
|
||||
createMockCategory('cat1', 'Category 1', [
|
||||
createMockTemplate('tpl1', 'Very Long Template Title That Should Be Truncated', {
|
||||
previewUrl: 'https://example.com/test.jpg',
|
||||
webpPreviewUrl: 'https://example.com/test.webp',
|
||||
}),
|
||||
]),
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 1000,
|
||||
}
|
||||
|
||||
mockUseCategories.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: mockCategoriesData,
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
mockUseActivates.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: { activities: [], total: 0, page: 1, limit: 10, totalPages: 0 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const { getByText } = render(<HomeScreen />)
|
||||
|
||||
await waitFor(() => {
|
||||
const title = getByText('Very Long Template Title That Should Be Truncated')
|
||||
expect(title).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('CRITICAL: Category Selection Logic', () => {
|
||||
it('should auto-select first category when categories load', async () => {
|
||||
const mockCategoriesData = {
|
||||
categories: [
|
||||
createMockCategory('cat1', 'First Category', [
|
||||
createMockTemplate('tpl1', 'Template 1', {
|
||||
previewUrl: 'https://example.com/test.jpg',
|
||||
webpPreviewUrl: 'https://example.com/test.webp',
|
||||
}),
|
||||
]),
|
||||
createMockCategory('cat2', 'Second Category', []),
|
||||
],
|
||||
total: 2,
|
||||
page: 1,
|
||||
limit: 1000,
|
||||
}
|
||||
|
||||
mockUseCategories.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: mockCategoriesData,
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
mockUseActivates.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: { activities: [], total: 0, page: 1, limit: 10, totalPages: 0 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const { getByText } = render(<HomeScreen />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('First Category')).toBeTruthy()
|
||||
expect(getByText('Second Category')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle tab press when categories array is empty (set categoryId to null)', async () => {
|
||||
mockUseCategories.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: { categories: [], total: 0, page: 1, limit: 1000 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
mockUseActivates.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: { activities: [], total: 0, page: 1, limit: 10, totalPages: 0 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const { getByText } = render(<HomeScreen />)
|
||||
|
||||
// When categories are empty, the component shows empty state instead of default tabs
|
||||
await waitFor(() => {
|
||||
expect(getByText('暂无分类数据')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('CRITICAL: URL Params Handling - categoryId', () => {
|
||||
it('should select category from URL params on mount', async () => {
|
||||
const mockCategoriesData = {
|
||||
categories: [
|
||||
createMockCategory('cat1', 'Category 1', []),
|
||||
createMockCategory('cat2', 'Category 2', [
|
||||
createMockTemplate('tpl1', 'Template in Cat 2', {
|
||||
previewUrl: 'https://example.com/test.jpg',
|
||||
webpPreviewUrl: 'https://example.com/test.webp',
|
||||
}),
|
||||
]),
|
||||
],
|
||||
total: 2,
|
||||
page: 1,
|
||||
limit: 1000,
|
||||
}
|
||||
|
||||
mockUseCategories.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: mockCategoriesData,
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
mockUseActivates.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: { activities: [], total: 0, page: 1, limit: 10, totalPages: 0 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
// Mock URL params with categoryId
|
||||
jest.doMock('expo-router', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
}),
|
||||
useLocalSearchParams: () => ({ categoryId: 'cat2' }),
|
||||
}))
|
||||
|
||||
const { getByText } = render(<HomeScreen />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Category 2')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not update when categoryId param is undefined', async () => {
|
||||
const mockCategoriesData = {
|
||||
categories: [
|
||||
createMockCategory('cat1', 'Category 1', [
|
||||
createMockTemplate('tpl1', 'Template 1', {
|
||||
previewUrl: 'https://example.com/test.jpg',
|
||||
webpPreviewUrl: 'https://example.com/test.webp',
|
||||
}),
|
||||
]),
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 1000,
|
||||
}
|
||||
|
||||
mockUseCategories.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: mockCategoriesData,
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
mockUseActivates.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: { activities: [], total: 0, page: 1, limit: 10, totalPages: 0 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const { getByText } = render(<HomeScreen />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Category 1')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('CRITICAL: Template Filtering', () => {
|
||||
it('should filter out .mp4 video templates', async () => {
|
||||
const mockCategoriesData = {
|
||||
categories: [
|
||||
createMockCategory('cat1', 'Category 1', [
|
||||
createMockTemplate('tpl1', 'Image Template', {
|
||||
previewUrl: 'https://example.com/image.jpg',
|
||||
webpPreviewUrl: 'https://example.com/image.webp',
|
||||
}),
|
||||
createMockTemplate('tpl2', 'MP4 Video Template', {
|
||||
previewUrl: 'https://example.com/video.mp4',
|
||||
webpPreviewUrl: 'https://example.com/video.mp4',
|
||||
}),
|
||||
]),
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 1000,
|
||||
}
|
||||
|
||||
mockUseCategories.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: mockCategoriesData,
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
mockUseActivates.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: { activities: [], total: 0, page: 1, limit: 10, totalPages: 0 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const { getByText, queryByText } = render(<HomeScreen />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Image Template')).toBeTruthy()
|
||||
expect(queryByText('MP4 Video Template')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
it('should filter out .mov video templates', async () => {
|
||||
const mockCategoriesData = {
|
||||
categories: [
|
||||
createMockCategory('cat1', 'Category 1', [
|
||||
createMockTemplate('tpl1', 'Image Template', {
|
||||
previewUrl: 'https://example.com/image.jpg',
|
||||
webpPreviewUrl: 'https://example.com/image.webp',
|
||||
}),
|
||||
createMockTemplate('tpl2', 'MOV Video Template', {
|
||||
previewUrl: 'https://example.com/video.mov',
|
||||
webpPreviewUrl: 'https://example.com/video.mov',
|
||||
}),
|
||||
]),
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 1000,
|
||||
}
|
||||
|
||||
mockUseCategories.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: mockCategoriesData,
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
mockUseActivates.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: { activities: [], total: 0, page: 1, limit: 10, totalPages: 0 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const { getByText, queryByText } = render(<HomeScreen />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Image Template')).toBeTruthy()
|
||||
expect(queryByText('MOV Video Template')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
it('should filter out .webm video templates', async () => {
|
||||
const mockCategoriesData = {
|
||||
categories: [
|
||||
createMockCategory('cat1', 'Category 1', [
|
||||
createMockTemplate('tpl1', 'Image Template', {
|
||||
previewUrl: 'https://example.com/image.jpg',
|
||||
webpPreviewUrl: 'https://example.com/image.webp',
|
||||
}),
|
||||
createMockTemplate('tpl2', 'WebM Video Template', {
|
||||
previewUrl: 'https://example.com/video.webm',
|
||||
webpPreviewUrl: 'https://example.com/video.webm',
|
||||
}),
|
||||
]),
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 1000,
|
||||
}
|
||||
|
||||
mockUseCategories.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: mockCategoriesData,
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
mockUseActivates.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: { activities: [], total: 0, page: 1, limit: 10, totalPages: 0 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const { getByText, queryByText } = render(<HomeScreen />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Image Template')).toBeTruthy()
|
||||
expect(queryByText('WebM Video Template')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
it('should display image templates', async () => {
|
||||
const mockCategoriesData = {
|
||||
categories: [
|
||||
createMockCategory('cat1', 'Category 1', [
|
||||
createMockTemplate('tpl1', 'JPG Image', {
|
||||
previewUrl: 'https://example.com/image.jpg',
|
||||
webpPreviewUrl: 'https://example.com/image.webp',
|
||||
}),
|
||||
createMockTemplate('tpl2', 'PNG Image', {
|
||||
previewUrl: 'https://example.com/image.png',
|
||||
webpPreviewUrl: 'https://example.com/image.png',
|
||||
}),
|
||||
]),
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 1000,
|
||||
}
|
||||
|
||||
mockUseCategories.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: mockCategoriesData,
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
mockUseActivates.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: { activities: [], total: 0, page: 1, limit: 10, totalPages: 0 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const { getByText } = render(<HomeScreen />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('JPG Image')).toBeTruthy()
|
||||
expect(getByText('PNG Image')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle templates with missing previewUrl', async () => {
|
||||
const template = createMockTemplate('tpl1', 'Template with Missing URL')
|
||||
// @ts-ignore - testing missing previewUrl
|
||||
delete template.previewUrl
|
||||
// @ts-ignore - testing missing webpPreviewUrl
|
||||
delete template.webpPreviewUrl
|
||||
|
||||
const mockCategoriesData = {
|
||||
categories: [
|
||||
createMockCategory('cat1', 'Category 1', [template]),
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 1000,
|
||||
}
|
||||
|
||||
mockUseCategories.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: mockCategoriesData,
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
mockUseActivates.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: { activities: [], total: 0, page: 1, limit: 10, totalPages: 0 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const { getByText } = render(<HomeScreen />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Template with Missing URL')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('IMPORTANT: Loading States', () => {
|
||||
it('should show HomeSkeleton when categoriesLoading is true', async () => {
|
||||
mockUseCategories.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: undefined,
|
||||
loading: true,
|
||||
error: null,
|
||||
})
|
||||
|
||||
mockUseActivates.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: { activities: [], total: 0, page: 1, limit: 10, totalPages: 0 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const { getByText } = render(<HomeScreen />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Popcore')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
it('should hide tabs when loading', async () => {
|
||||
mockUseCategories.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: undefined,
|
||||
loading: true,
|
||||
error: null,
|
||||
})
|
||||
|
||||
mockUseActivates.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: { activities: [], total: 0, page: 1, limit: 10, totalPages: 0 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const { queryByText } = render(<HomeScreen />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByText('Featured')).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('IMPORTANT: Empty States', () => {
|
||||
it('should show empty state when categories array is empty', async () => {
|
||||
mockUseCategories.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: { categories: [], total: 0, page: 1, limit: 1000 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
mockUseActivates.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: { activities: [], total: 0, page: 1, limit: 10, totalPages: 0 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const { getByText } = render(<HomeScreen />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('暂无分类数据')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show empty templates message when category has no templates', async () => {
|
||||
const mockCategoriesData = {
|
||||
categories: [
|
||||
createMockCategory('cat1', 'Empty Category', []),
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 1000,
|
||||
}
|
||||
|
||||
mockUseCategories.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: mockCategoriesData,
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
mockUseActivates.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: { activities: [], total: 0, page: 1, limit: 10, totalPages: 0 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const { getByText } = render(<HomeScreen />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('该分类暂无模板')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
it('should call loadCategories when retry button is pressed', async () => {
|
||||
const mockLoad = jest.fn().mockResolvedValue(undefined)
|
||||
|
||||
mockUseCategories.mockReturnValue({
|
||||
load: mockLoad,
|
||||
data: { categories: [], total: 0, page: 1, limit: 1000 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
mockUseActivates.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: { activities: [], total: 0, page: 1, limit: 10, totalPages: 0 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const { getByText } = render(<HomeScreen />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('暂无分类数据')).toBeTruthy()
|
||||
expect(getByText('重新加载')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('IMPORTANT: Sticky Tabs Behavior', () => {
|
||||
it('should render tabs without sticky state initially', async () => {
|
||||
const mockCategoriesData = {
|
||||
categories: [
|
||||
createMockCategory('cat1', 'Category 1', [
|
||||
createMockTemplate('tpl1', 'Template 1', {
|
||||
previewUrl: 'https://example.com/image.jpg',
|
||||
webpPreviewUrl: 'https://example.com/image.webp',
|
||||
}),
|
||||
]),
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 1000,
|
||||
}
|
||||
|
||||
mockUseCategories.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: mockCategoriesData,
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
mockUseActivates.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: { activities: [], total: 0, page: 1, limit: 10, totalPages: 0 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const { getByText } = render(<HomeScreen />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Category 1')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Component Integration', () => {
|
||||
it('should load categories and activates on mount', async () => {
|
||||
const mockLoadCategories = jest.fn().mockResolvedValue(undefined)
|
||||
const mockLoadActivates = jest.fn().mockResolvedValue(undefined)
|
||||
|
||||
mockUseCategories.mockReturnValue({
|
||||
load: mockLoadCategories,
|
||||
data: { categories: [], total: 0, page: 1, limit: 1000 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
mockUseActivates.mockReturnValue({
|
||||
load: mockLoadActivates,
|
||||
data: { activities: [], total: 0, page: 1, limit: 10, totalPages: 0 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
render(<HomeScreen />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockLoadCategories).toHaveBeenCalled()
|
||||
expect(mockLoadActivates).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should render app title', async () => {
|
||||
mockUseCategories.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: { categories: [], total: 0, page: 1, limit: 1000 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
mockUseActivates.mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(undefined),
|
||||
data: { activities: [], total: 0, page: 1, limit: 10, totalPages: 0 },
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const { getByText } = render(<HomeScreen />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Popcore')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -53,7 +53,7 @@ const Card = ReactMemo(({ card, cardWidth, t, onPress }: {
|
||||
]}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: card.webpPreviewUrl }}
|
||||
source={{ uri: card.webpPreviewUrl || card.previewUrl }}
|
||||
style={[
|
||||
styles.cardImage,
|
||||
{ aspectRatio },
|
||||
|
||||
348
app/(tabs)/video.test.tsx
Normal file
348
app/(tabs)/video.test.tsx
Normal file
@@ -0,0 +1,348 @@
|
||||
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 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useRef, useEffect, useCallback, memo } from 'react'
|
||||
import React, { useState, useRef, useEffect, useCallback, memo } from 'react'
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
@@ -76,7 +76,7 @@ const calculateImageSize = (
|
||||
: { width: videoHeight * imageAspectRatio, height: videoHeight }
|
||||
}
|
||||
|
||||
const VideoItem = memo(({ item, videoHeight }: { item: TemplateDetail; videoHeight: number }) => {
|
||||
export const VideoItem = memo(({ item, videoHeight }: { item: TemplateDetail; videoHeight: number }) => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const [imageSize, setImageSize] = useState<{ width: number; height: number } | null>(null)
|
||||
@@ -91,14 +91,14 @@ const VideoItem = memo(({ item, videoHeight }: { item: TemplateDetail; videoHeig
|
||||
const handlePress = useCallback(() => {
|
||||
router.push({
|
||||
pathname: '/generateVideo' as any,
|
||||
params: { template: JSON.stringify(item) },
|
||||
params: { templateId: item.id },
|
||||
})
|
||||
}, [router, item])
|
||||
}, [router, item.id])
|
||||
|
||||
const imageStyle = calculateImageSize(imageSize, videoHeight)
|
||||
|
||||
// 优先使用 WebP 格式(支持动画),回退到普通预览图
|
||||
const displayUrl = item.webpPreviewUrl || item.previewUrl
|
||||
const displayUrl = item.webpHighPreviewUrl || item.webpPreviewUrl || item.previewUrl
|
||||
|
||||
return (
|
||||
<View style={[styles.videoContainer, { height: videoHeight }]}>
|
||||
|
||||
Reference in New Issue
Block a user