import React from 'react'
import { render, fireEvent, waitFor } from '@testing-library/react-native'
import { View, Text } from 'react-native'
import TemplateDetailScreen from './templateDetail'
// Mock expo-router
jest.mock('expo-router', () => ({
useRouter: jest.fn(() => ({
back: jest.fn(),
push: jest.fn(),
})),
useLocalSearchParams: jest.fn(() => ({ id: 'test-template-id' })),
}))
// 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 components
jest.mock('@/components/icon', () => ({
LeftArrowIcon: () => null,
}))
jest.mock('@/components/SearchResultsGrid', () => ({
__esModule: true,
default: () => null,
}))
jest.mock('@/components/DynamicForm', () => {
const { View, Text } = require('react-native')
return {
__esModule: true,
DynamicForm: React.forwardRef(({ formSchema, onSubmit, onOpenDrawer }: any, ref: any) => (
onSubmit({})}>Submit Form
)),
}
})
jest.mock('@/components/ui/Toast', () => ({
__esModule: true,
default: {
show: jest.fn(),
showLoading: jest.fn(),
hideLoading: jest.fn(),
},
}))
jest.mock('@/components/drawer/UploadReferenceImageDrawer', () => ({
__esModule: true,
default: () => null,
}))
jest.mock('@/components/blocks/ui/SocialActionBar', () => ({
__esModule: true,
SocialActionBar: jest.fn(() => null),
}))
// Mock hooks
jest.mock('@/hooks', () => ({
useTemplateActions: jest.fn(() => ({
runTemplate: jest.fn().mockResolvedValue({ generationId: 'gen-123' }),
})),
useTemplateDetail: jest.fn(() => ({
data: {
id: 'test-template-id',
title: 'Test Template',
titleEn: 'Test Template',
formSchema: {
startNodes: [
{
id: 'node_1',
type: 'text',
data: { label: '文本节点', text: '默认文本' },
},
],
},
},
loading: false,
error: null,
execute: jest.fn(),
})),
useTemplateGenerations: jest.fn(() => ({
generations: [],
loading: false,
execute: jest.fn(),
})),
useTemplateLike: jest.fn(() => ({
liked: false,
loading: false,
error: null,
like: jest.fn().mockResolvedValue({}),
unlike: jest.fn().mockResolvedValue({}),
checkLiked: jest.fn().mockResolvedValue({}),
})),
useTemplateFavorite: jest.fn(() => ({
favorited: false,
loading: false,
error: null,
favorite: jest.fn().mockResolvedValue({ data: null, error: null }),
unfavorite: jest.fn().mockResolvedValue({ data: null, error: null }),
checkFavorited: jest.fn().mockResolvedValue({ data: null, error: null }),
})),
}))
import { useTemplateActions, useTemplateDetail, useTemplateGenerations, useTemplateLike, useTemplateFavorite } from '@/hooks'
import Toast from '@/components/ui/Toast'
const mockUseTemplateActions = useTemplateActions as jest.MockedFunction
const mockUseTemplateDetail = useTemplateDetail as jest.MockedFunction
const mockUseTemplateGenerations = useTemplateGenerations as jest.MockedFunction
const mockUseTemplateLike = useTemplateLike as jest.MockedFunction
const mockUseTemplateFavorite = useTemplateFavorite as jest.MockedFunction
describe('TemplateDetail Screen', () => {
beforeEach(() => {
jest.clearAllMocks()
})
describe('Loading State', () => {
it('should show loading indicator when template is loading', () => {
mockUseTemplateDetail.mockReturnValue({
data: undefined,
loading: true,
error: null,
execute: jest.fn(),
} as any)
const { getByTestId } = render()
// Should show loading state
expect(mockUseTemplateDetail).toHaveBeenCalled()
})
it('should not show loading when template data is loaded', () => {
mockUseTemplateDetail.mockReturnValue({
data: {
id: 'test-id',
title: 'Test Template',
titleEn: 'Test Template',
},
loading: false,
error: null,
execute: jest.fn(),
} as any)
mockUseTemplateGenerations.mockReturnValue({
generations: [],
loading: false,
execute: jest.fn(),
} as any)
const { queryByText } = render()
expect(queryByText('加载中...')).toBeNull()
})
})
describe('Error State', () => {
it('should show error message when template loading fails', () => {
mockUseTemplateDetail.mockReturnValue({
data: undefined,
loading: false,
error: { message: 'Failed to load' },
execute: jest.fn(),
} as any)
const { getByText } = render()
expect(getByText('加载失败,请返回重试')).toBeTruthy()
})
it('should show retry button on error', () => {
mockUseTemplateDetail.mockReturnValue({
data: undefined,
loading: false,
error: { message: 'Failed to load' },
execute: jest.fn(),
} as any)
const { getByText } = render()
expect(getByText('返回')).toBeTruthy()
})
})
describe('Template Display', () => {
it('should display template title', () => {
const mockData = {
id: 'test-id',
title: 'Test Template',
titleEn: 'Test Template',
}
mockUseTemplateDetail.mockReturnValue({
data: mockData,
loading: false,
error: null,
execute: jest.fn(),
} as any)
mockUseTemplateGenerations.mockReturnValue({
generations: [],
loading: false,
execute: jest.fn(),
} as any)
const { getAllByText } = render()
// Title appears in both header and main title section
const titles = getAllByText('Test Template')
expect(titles.length).toBeGreaterThan(0)
})
it('should display start creating button when no formSchema', () => {
mockUseTemplateDetail.mockReturnValue({
data: {
id: 'test-id',
title: 'Test Template',
formSchema: {
startNodes: [],
},
},
loading: false,
error: null,
execute: jest.fn(),
} as any)
mockUseTemplateGenerations.mockReturnValue({
generations: [],
loading: false,
execute: jest.fn(),
} as any)
const { getByText } = render()
expect(getByText('templateDetail.startCreating')).toBeTruthy()
})
it('should display generations section', () => {
mockUseTemplateDetail.mockReturnValue({
data: {
id: 'test-id',
title: 'Test Template',
},
loading: false,
error: null,
execute: jest.fn(),
} as any)
mockUseTemplateGenerations.mockReturnValue({
generations: [],
loading: false,
execute: jest.fn(),
} as any)
const { getByText } = render()
expect(getByText('templateDetail.generations')).toBeTruthy()
})
})
describe('DynamicForm Integration', () => {
it('should show form inline when formSchema exists', () => {
mockUseTemplateDetail.mockReturnValue({
data: {
id: 'test-id',
title: 'Test Template',
formSchema: {
startNodes: [
{
id: 'node_1',
type: 'text',
data: { label: '文本' },
},
],
},
},
loading: false,
error: null,
execute: jest.fn(),
} as any)
mockUseTemplateGenerations.mockReturnValue({
generations: [],
loading: false,
execute: jest.fn(),
} as any)
const { getByTestId } = render()
// Form should be displayed inline
expect(getByTestId('dynamic-form')).toBeTruthy()
})
it('should navigate to generateVideo when start creating is pressed with no formSchema', () => {
const mockPush = jest.fn()
const mockRouter = { push: mockPush, back: jest.fn() }
// Update the router mock
jest.spyOn(require('expo-router'), 'useRouter').mockReturnValue(mockRouter)
mockUseTemplateDetail.mockReturnValue({
data: {
id: 'test-id',
title: 'Test Template',
formSchema: {
startNodes: [],
},
},
loading: false,
error: null,
execute: jest.fn(),
} as any)
mockUseTemplateGenerations.mockReturnValue({
generations: [],
loading: false,
execute: jest.fn(),
} as any)
const { getByText } = render()
const startButton = getByText('templateDetail.startCreating')
fireEvent.press(startButton)
// Should have pushed to generateVideo route
expect(mockPush).toHaveBeenCalled()
})
it('should handle form submission correctly', async () => {
const mockRunTemplate = jest.fn().mockResolvedValue({
generationId: 'gen-123',
})
mockUseTemplateActions.mockReturnValue({
runTemplate: mockRunTemplate,
loading: false,
error: null,
} as any)
mockUseTemplateDetail.mockReturnValue({
data: {
id: 'test-id',
title: 'Test Template',
formSchema: {
startNodes: [
{
id: 'node_1',
type: 'text',
data: { label: '文本' },
},
],
},
},
loading: false,
error: null,
execute: jest.fn(),
} as any)
mockUseTemplateGenerations.mockReturnValue({
generations: [],
loading: false,
execute: jest.fn(),
} as any)
const { getByTestId, getByText } = render()
// Form is shown inline
const form = getByTestId('dynamic-form')
expect(form).toBeTruthy()
// Submit form via the mocked submit button
const submitButton = getByText('Submit Form')
fireEvent.press(submitButton)
await waitFor(() => {
expect(mockRunTemplate).toHaveBeenCalledWith({
templateId: 'test-template-id',
data: {},
})
expect(Toast.show).toHaveBeenCalledWith({
title: 'templateDetail.generationStarted',
})
})
})
it('should handle form submission errors', async () => {
const mockRunTemplate = jest.fn().mockResolvedValue({
error: { message: 'Submission failed' },
})
mockUseTemplateActions.mockReturnValue({
runTemplate: mockRunTemplate,
loading: false,
error: null,
} as any)
mockUseTemplateDetail.mockReturnValue({
data: {
id: 'test-id',
title: 'Test Template',
formSchema: {
startNodes: [
{
id: 'node_1',
type: 'text',
data: { label: '文本' },
},
],
},
},
loading: false,
error: null,
execute: jest.fn(),
} as any)
mockUseTemplateGenerations.mockReturnValue({
generations: [],
loading: false,
execute: jest.fn(),
} as any)
const { getByTestId, getByText } = render()
// Form is shown inline
expect(getByTestId('dynamic-form')).toBeTruthy()
const submitButton = getByText('Submit Form')
fireEvent.press(submitButton)
await waitFor(() => {
expect(mockRunTemplate).toHaveBeenCalled()
})
})
})
describe('Generations Display', () => {
it('should display generations when available', () => {
const mockGenerations = [
{
id: 'gen-1',
template: { title: 'Template 1' },
resultUrl: ['http://example.com/image1.jpg'],
originalUrl: 'http://example.com/thumb1.jpg',
},
{
id: 'gen-2',
template: { titleEn: 'Template 2' },
resultUrl: ['http://example.com/image2.jpg'],
originalUrl: 'http://example.com/thumb2.jpg',
},
]
mockUseTemplateDetail.mockReturnValue({
data: {
id: 'test-id',
title: 'Test Template',
},
loading: false,
error: null,
execute: jest.fn(),
} as any)
mockUseTemplateGenerations.mockReturnValue({
generations: mockGenerations as any,
loading: false,
execute: jest.fn(),
} as any)
const { getByText } = render()
expect(getByText('templateDetail.generations')).toBeTruthy()
})
it('should show loading indicator when loading generations', () => {
mockUseTemplateDetail.mockReturnValue({
data: {
id: 'test-id',
title: 'Test Template',
},
loading: false,
error: null,
execute: jest.fn(),
} as any)
mockUseTemplateGenerations.mockReturnValue({
generations: [{ id: 'gen-1' }] as any,
loading: true,
execute: jest.fn(),
} as any)
const { getByText } = render()
expect(getByText('加载中...')).toBeTruthy()
})
})
describe('Data Fetching', () => {
it('should fetch template detail and generations on mount', () => {
const mockExecuteDetail = jest.fn()
const mockExecuteGenerations = jest.fn()
mockUseTemplateDetail.mockReturnValue({
data: {
id: 'test-id',
title: 'Test Template',
},
loading: false,
error: null,
execute: mockExecuteDetail,
} as any)
mockUseTemplateGenerations.mockReturnValue({
generations: [],
loading: false,
execute: mockExecuteGenerations,
} as any)
render()
expect(mockExecuteDetail).toHaveBeenCalledWith({ id: 'test-template-id' })
expect(mockExecuteGenerations).toHaveBeenCalledWith({
templateId: 'test-template-id',
page: 1,
limit: 20,
})
})
})
})