552 lines
15 KiB
TypeScript
552 lines
15 KiB
TypeScript
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 }) => (
|
|
<View>{children}</View>
|
|
),
|
|
}))
|
|
|
|
// 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<typeof useRouter>
|
|
const mockUseLocalSearchParams = useLocalSearchParams as jest.MockedFunction<typeof useLocalSearchParams>
|
|
const mockUseTemplateActions = useTemplateActions as jest.MockedFunction<typeof useTemplateActions>
|
|
const mockUseTemplateDetail = useTemplateDetail as jest.MockedFunction<typeof useTemplateDetail>
|
|
|
|
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(<GenerateVideoScreen />)
|
|
|
|
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(<GenerateVideoScreen />)
|
|
|
|
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(<GenerateVideoScreen />)
|
|
|
|
// 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(<GenerateVideoScreen />)
|
|
|
|
// 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(<GenerateVideoScreen />)
|
|
|
|
// 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(<GenerateVideoScreen />)
|
|
|
|
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(<GenerateVideoScreen />)
|
|
|
|
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(<GenerateVideoScreen />)
|
|
|
|
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(<GenerateVideoScreen />)
|
|
|
|
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(<GenerateVideoScreen />)
|
|
|
|
// 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(<GenerateVideoScreen />)
|
|
|
|
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(<GenerateVideoScreen />)
|
|
|
|
// 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(<GenerateVideoScreen />)
|
|
|
|
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(<GenerateVideoScreen />)
|
|
|
|
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(<GenerateVideoScreen />)
|
|
|
|
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(<GenerateVideoScreen />)
|
|
|
|
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(<GenerateVideoScreen />)
|
|
|
|
// 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(<GenerateVideoScreen />)
|
|
|
|
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(<GenerateVideoScreen />)
|
|
|
|
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(<GenerateVideoScreen />)
|
|
|
|
// Should default to 10
|
|
const priceElements = getAllByText('10')
|
|
expect(priceElements.length).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
})
|