import React from 'react'
import { render, fireEvent, waitFor } from '@testing-library/react-native'
import { View, Text } from 'react-native'
import GenerateVideoScreen from './generateVideo'
// Mock expo-router
jest.mock('expo-router', () => ({
useRouter: jest.fn(() => ({
back: jest.fn(),
})),
useLocalSearchParams: 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 }) => (
{children}
),
}))
// Mock expo-linear-gradient
jest.mock('expo-linear-gradient', () => ({
LinearGradient: 'LinearGradient',
}))
// Mock expo-image
jest.mock('expo-image', () => ({
Image: 'Image',
}))
// Mock components
jest.mock('@/components/icon', () => ({
LeftArrowIcon: 'LeftArrowIcon',
UploadIcon: 'UploadIcon',
WhitePointsIcon: 'WhitePointsIcon',
}))
jest.mock('@/components/drawer/UploadReferenceImageDrawer', () => ({
__esModule: true,
default: 'UploadReferenceImageDrawer',
}))
jest.mock('@/components/ui', () => ({
StartGeneratingNotification: 'StartGeneratingNotification',
}))
jest.mock('@/components/ui/Toast', () => ({
__esModule: true,
default: {
show: jest.fn(),
showLoading: jest.fn(),
hideLoading: jest.fn(),
},
}))
// Mock hooks
jest.mock('@/hooks/use-template-actions', () => ({
useTemplateActions: jest.fn(() => ({
runTemplate: jest.fn().mockResolvedValue({ generationId: 'gen-123' }),
loading: false,
error: null,
})),
}))
jest.mock('@/hooks/use-template-detail', () => ({
useTemplateDetail: jest.fn(() => ({
data: undefined,
loading: false,
error: null,
execute: jest.fn(),
})),
}))
jest.mock('@/lib/uploadFile', () => ({
uploadFile: jest.fn().mockResolvedValue('https://example.com/uploaded.jpg'),
}))
import { useRouter, useLocalSearchParams } from 'expo-router'
import { useTemplateActions } from '@/hooks/use-template-actions'
import { useTemplateDetail } from '@/hooks/use-template-detail'
import { uploadFile } from '@/lib/uploadFile'
import Toast from '@/components/ui/Toast'
const mockUseRouter = useRouter as jest.MockedFunction
const mockUseLocalSearchParams = useLocalSearchParams as jest.MockedFunction
const mockUseTemplateActions = useTemplateActions as jest.MockedFunction
const mockUseTemplateDetail = useTemplateDetail as jest.MockedFunction
describe('GenerateVideo Screen', () => {
const mockTemplateDetail = {
id: 'template-123',
title: 'Test Template',
titleEn: 'Test Template',
thumbnailUrl: 'https://example.com/thumbnail.jpg',
coverImageUrl: 'https://example.com/cover.jpg',
price: 10,
formSchema: {
startNodes: [
{
id: 'node_1',
type: 'text',
},
{
id: 'node_2',
type: 'image',
},
],
},
}
beforeEach(() => {
jest.clearAllMocks()
mockUseLocalSearchParams.mockReturnValue({ templateId: 'template-123' } as any)
})
describe('Data Fetching', () => {
it('should fetch template detail on mount when templateId is provided', () => {
const mockExecute = jest.fn()
mockUseTemplateDetail.mockReturnValue({
data: undefined,
loading: false,
error: null,
execute: mockExecute,
} as any)
render()
expect(mockExecute).toHaveBeenCalledWith({ id: 'template-123' })
})
it('should not fetch template detail when templateId is not provided', () => {
const mockExecute = jest.fn()
mockUseLocalSearchParams.mockReturnValue({} as any)
mockUseTemplateDetail.mockReturnValue({
data: undefined,
loading: false,
error: null,
execute: mockExecute,
} as any)
render()
expect(mockExecute).not.toHaveBeenCalled()
})
it('should set previewImageUri when template detail is loaded', () => {
mockUseTemplateDetail.mockReturnValue({
data: mockTemplateDetail,
loading: false,
error: null,
execute: jest.fn(),
} as any)
render()
// Component should render without errors
// previewImageUri is internal state, so we verify through UI
})
it('should handle template loading state', () => {
mockUseTemplateDetail.mockReturnValue({
data: undefined,
loading: true,
error: null,
execute: jest.fn(),
} as any)
const { getByText } = render()
// Should show template title area
expect(getByText('generateVideo.uploadReference')).toBeTruthy()
})
it('should handle template error state', () => {
mockUseTemplateDetail.mockReturnValue({
data: undefined,
loading: false,
error: { message: 'Failed to load template' },
execute: jest.fn(),
} as any)
const { queryByText } = render()
// Should still render the UI if template was loaded before error
// or show empty state
expect(queryByText('generateVideo.uploadReference')).toBeTruthy()
})
})
describe('Form Rendering', () => {
it('should display template title when loaded', () => {
mockUseTemplateDetail.mockReturnValue({
data: mockTemplateDetail,
loading: false,
error: null,
execute: jest.fn(),
} as any)
const { getAllByText } = render()
const titles = getAllByText('Test Template')
expect(titles.length).toBeGreaterThan(0)
})
it('should display template thumbnail', () => {
mockUseTemplateDetail.mockReturnValue({
data: mockTemplateDetail,
loading: false,
error: null,
execute: jest.fn(),
} as any)
const { getByText } = render()
expect(getByText('generateVideo.uploadReference')).toBeTruthy()
})
it('should display upload reference button', () => {
mockUseTemplateDetail.mockReturnValue({
data: mockTemplateDetail,
loading: false,
error: null,
execute: jest.fn(),
} as any)
const { getByText } = render()
expect(getByText('generateVideo.uploadReference')).toBeTruthy()
})
it('should display description input field', () => {
mockUseTemplateDetail.mockReturnValue({
data: mockTemplateDetail,
loading: false,
error: null,
execute: jest.fn(),
} as any)
const { getByPlaceholderText } = render()
expect(getByPlaceholderText('generateVideo.descriptionPlaceholder')).toBeTruthy()
})
it('should display generate button with correct price', () => {
mockUseTemplateDetail.mockReturnValue({
data: mockTemplateDetail,
loading: false,
error: null,
execute: jest.fn(),
} as any)
const { getAllByText } = render()
// Price should be displayed (10 in this case)
const priceElements = getAllByText('10')
expect(priceElements.length).toBeGreaterThan(0)
})
})
describe('Form Submission', () => {
it('should show error when image is required but not uploaded', async () => {
mockUseTemplateDetail.mockReturnValue({
data: mockTemplateDetail,
loading: false,
error: null,
execute: jest.fn(),
} as any)
const mockRunTemplate = jest.fn().mockResolvedValue({
generationId: 'gen-123',
error: null,
})
mockUseTemplateActions.mockReturnValue({
runTemplate: mockRunTemplate,
loading: false,
error: null,
} as any)
const { getByText } = render()
const generateButton = getByText('generateVideo.generate')
fireEvent.press(generateButton)
await waitFor(() => {
expect(Toast.show).toHaveBeenCalledWith({
title: 'generateVideo.pleaseUploadImage',
})
})
})
it('should call runTemplate with correct parameters when form is valid', async () => {
// Use template without image node from the start
const templateWithoutImageNode = {
...mockTemplateDetail,
formSchema: {
startNodes: [
{
id: 'node_1',
type: 'text',
},
],
},
}
const mockExecute = jest.fn()
mockUseTemplateDetail.mockReturnValue({
data: templateWithoutImageNode,
loading: false,
error: null,
execute: mockExecute,
} as any)
const mockRunTemplate = jest.fn().mockResolvedValue({
generationId: 'gen-123',
error: null,
})
mockUseTemplateActions.mockReturnValue({
runTemplate: mockRunTemplate,
loading: false,
error: null,
} as any)
const { getByText, getByPlaceholderText } = render()
// Fill description
const descriptionInput = getByPlaceholderText('generateVideo.descriptionPlaceholder')
fireEvent.changeText(descriptionInput, 'Test description')
const generateButton = getByText('generateVideo.generate')
fireEvent.press(generateButton)
await waitFor(() => {
expect(mockRunTemplate).toHaveBeenCalledWith({
templateId: 'template-123',
data: {
node_1: 'Test description',
},
})
})
})
it('should show loading state during generation', () => {
mockUseTemplateDetail.mockReturnValue({
data: {
...mockTemplateDetail,
formSchema: {
startNodes: [{ id: 'node_1', type: 'text' }],
},
},
loading: false,
error: null,
execute: jest.fn(),
} as any)
mockUseTemplateActions.mockReturnValue({
runTemplate: jest.fn(),
loading: true,
error: null,
} as any)
const { getByText } = render()
expect(getByText('generateVideo.generating')).toBeTruthy()
})
it('should handle generation errors', async () => {
mockUseTemplateDetail.mockReturnValue({
data: {
...mockTemplateDetail,
formSchema: {
startNodes: [{ id: 'node_1', type: 'text' }],
},
},
loading: false,
error: null,
execute: jest.fn(),
} as any)
mockUseTemplateActions.mockReturnValue({
runTemplate: jest.fn().mockResolvedValue({
generationId: null,
error: { message: 'Generation failed' },
}),
loading: false,
error: null,
} as any)
const { getByText, getByPlaceholderText } = render()
const descriptionInput = getByPlaceholderText('generateVideo.descriptionPlaceholder')
fireEvent.changeText(descriptionInput, 'Test description')
const generateButton = getByText('generateVideo.generate')
fireEvent.press(generateButton)
await waitFor(() => {
expect(Toast.show).toHaveBeenCalledWith({
title: 'Generation failed',
})
})
})
it('should show notification and navigate back on successful generation', async () => {
const mockBack = jest.fn()
mockUseRouter.mockReturnValue({ back: mockBack } as any)
mockUseTemplateDetail.mockReturnValue({
data: {
...mockTemplateDetail,
formSchema: {
startNodes: [{ id: 'node_1', type: 'text' }],
},
},
loading: false,
error: null,
execute: jest.fn(),
} as any)
mockUseTemplateActions.mockReturnValue({
runTemplate: jest.fn().mockResolvedValue({
generationId: 'gen-123',
error: null,
}),
loading: false,
error: null,
} as any)
const { getByText, getByPlaceholderText } = render()
const descriptionInput = getByPlaceholderText('generateVideo.descriptionPlaceholder')
fireEvent.changeText(descriptionInput, 'Test description')
const generateButton = getByText('generateVideo.generate')
fireEvent.press(generateButton)
await waitFor(() => {
expect(mockBack).toHaveBeenCalled()
}, { timeout: 4000 })
})
})
describe('Image Upload', () => {
it('should handle image upload', async () => {
mockUseTemplateDetail.mockReturnValue({
data: mockTemplateDetail,
loading: false,
error: null,
execute: jest.fn(),
} as any)
const { getByText } = render()
const uploadButton = getByText('generateVideo.uploadReference')
fireEvent.press(uploadButton)
// Upload flow is handled through drawer component
// The drawer is mocked, so we verify the button press
})
})
describe('Navigation', () => {
it('should navigate back when back button is pressed', () => {
const mockBack = jest.fn()
mockUseRouter.mockReturnValue({ back: mockBack } as any)
mockUseTemplateDetail.mockReturnValue({
data: mockTemplateDetail,
loading: false,
error: null,
execute: jest.fn(),
} as any)
render()
// Component should render successfully
// Back button navigation is tested implicitly
})
})
describe('Edge Cases', () => {
it('should handle template without formSchema', () => {
mockUseTemplateDetail.mockReturnValue({
data: {
...mockTemplateDetail,
formSchema: undefined,
},
loading: false,
error: null,
execute: jest.fn(),
} as any)
const { getByText } = render()
expect(getByText('generateVideo.uploadReference')).toBeTruthy()
})
it('should handle template with empty startNodes', () => {
mockUseTemplateDetail.mockReturnValue({
data: {
...mockTemplateDetail,
formSchema: {
startNodes: [],
},
},
loading: false,
error: null,
execute: jest.fn(),
} as any)
const { getByText } = render()
expect(getByText('generateVideo.uploadReference')).toBeTruthy()
})
it('should handle template without price', () => {
mockUseTemplateDetail.mockReturnValue({
data: {
...mockTemplateDetail,
price: undefined,
},
loading: false,
error: null,
execute: jest.fn(),
} as any)
const { getAllByText } = render()
// Should default to 10
const priceElements = getAllByText('10')
expect(priceElements.length).toBeGreaterThan(0)
})
})
})