feat: 重构测试结构并添加动态表单组件
主要更改: - 重构测试文件结构,删除旧的测试文件并添加新的测试覆盖 - 添加 DynamicForm 组件及其测试 - 更新 Jest 配置以支持新的测试结构 - 更新组件、抽屉和国际化文件 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
514
app/templateDetail.test.tsx
Normal file
514
app/templateDetail.test.tsx
Normal file
@@ -0,0 +1,514 @@
|
||||
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 }) => (
|
||||
<View>{children}</View>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock components
|
||||
jest.mock('@/components/icon', () => ({
|
||||
LeftArrowIcon: 'LeftArrowIcon',
|
||||
}))
|
||||
|
||||
jest.mock('@/components/SearchResultsGrid', () => ({
|
||||
__esModule: true,
|
||||
default: 'SearchResultsGrid',
|
||||
}))
|
||||
|
||||
jest.mock('@/components/DynamicForm', () => ({
|
||||
__esModule: true,
|
||||
DynamicForm: ({ formSchema, onSubmit }: any) => (
|
||||
<View testID="dynamic-form">
|
||||
<Text onPress={() => onSubmit({})}>Submit Form</Text>
|
||||
</View>
|
||||
),
|
||||
FormSchema: {},
|
||||
}))
|
||||
|
||||
jest.mock('@/components/ui/Toast', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
show: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// 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(),
|
||||
})),
|
||||
}))
|
||||
|
||||
import { useTemplateActions, useTemplateDetail, useTemplateGenerations } from '@/hooks'
|
||||
import Toast from '@/components/ui/Toast'
|
||||
|
||||
const mockUseTemplateActions = useTemplateActions as jest.MockedFunction<typeof useTemplateActions>
|
||||
const mockUseTemplateDetail = useTemplateDetail as jest.MockedFunction<typeof useTemplateDetail>
|
||||
const mockUseTemplateGenerations = useTemplateGenerations as jest.MockedFunction<typeof useTemplateGenerations>
|
||||
|
||||
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(<TemplateDetailScreen />)
|
||||
|
||||
// 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(<TemplateDetailScreen />)
|
||||
|
||||
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(<TemplateDetailScreen />)
|
||||
|
||||
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(<TemplateDetailScreen />)
|
||||
|
||||
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(<TemplateDetailScreen />)
|
||||
|
||||
// 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(<TemplateDetailScreen />)
|
||||
|
||||
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(<TemplateDetailScreen />)
|
||||
|
||||
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(<TemplateDetailScreen />)
|
||||
|
||||
// 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(<TemplateDetailScreen />)
|
||||
|
||||
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(<TemplateDetailScreen />)
|
||||
|
||||
// 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(<TemplateDetailScreen />)
|
||||
|
||||
// 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(<TemplateDetailScreen />)
|
||||
|
||||
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(<TemplateDetailScreen />)
|
||||
|
||||
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(<TemplateDetailScreen />)
|
||||
|
||||
expect(mockExecuteDetail).toHaveBeenCalledWith({ id: 'test-template-id' })
|
||||
expect(mockExecuteGenerations).toHaveBeenCalledWith({
|
||||
templateId: 'test-template-id',
|
||||
page: 1,
|
||||
limit: 20,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user