import React from 'react' import { render, fireEvent, waitFor } from '@testing-library/react-native' import { View, Pressable, TextInput, ActivityIndicator } from 'react-native' import { DynamicForm, type FormSchema } from './DynamicForm' // Mock expo-router jest.mock('expo-router', () => ({ useRouter: jest.fn(() => ({ back: jest.fn(), })), useLocalSearchParams: jest.fn(), })) // Mock expo-image jest.mock('expo-image', () => ({ Image: 'Image', })) // Mock react-i18next jest.mock('react-i18next', () => ({ useTranslation: jest.fn(() => ({ t: (key: string) => key, })), })) // Mock AIGenerationRecordDrawer BEFORE UploadReferenceImageDrawer jest.mock('./drawer/AIGenerationRecordDrawer', () => 'AIGenerationRecordDrawer') // Mock UploadReferenceImageDrawer jest.mock('./drawer/UploadReferenceImageDrawer', () => 'UploadReferenceImageDrawer') // Mock uploadFile jest.mock('@/lib/uploadFile', () => ({ uploadFile: jest.fn(), })) // Mock Toast jest.mock('./ui/Toast', () => ({ __esModule: true, default: { show: jest.fn(), showLoading: jest.fn(), hideLoading: jest.fn(), }, })) // Mock Button with proper React Native components jest.mock('./ui/button', () => ({ Button: ({ children, onPress, disabled, style, testID, ...props }: any) => ( {children} ), })) // Mock Text component to handle nested text properly jest.mock('./ui/Text', () => { const { Text } = require('react-native') return { __esModule: true, default: Text, } }) // Get mock functions const uploadFile = require('@/lib/uploadFile').uploadFile const Toast = require('./ui/Toast').default describe('DynamicForm Component', () => { const mockOnSubmit = jest.fn() beforeEach(() => { jest.clearAllMocks() }) describe('Rendering', () => { it('should render empty state when no startNodes', () => { const formSchema: FormSchema = { startNodes: [] } const { getByText } = render( ) expect(getByText('dynamicForm.noFields')).toBeTruthy() }) it('should render text input field correctly', () => { const formSchema: FormSchema = { startNodes: [ { id: 'node_1', type: 'text', data: { label: '文本节点', description: '请输入文本内容', }, }, ], } const { getByText, getByTestId } = render( ) expect(getByText('文本节点', { exact: false })).toBeTruthy() // Input placeholder is tested via testID instead expect(getByTestId('text-input-node_1')).toBeTruthy() }) it('should render image upload field correctly', () => { const formSchema: FormSchema = { startNodes: [ { id: 'node_1', type: 'image', data: { label: '图片节点', }, }, ], } const { getByText } = render( ) expect(getByText('图片节点', { exact: false })).toBeTruthy() expect(getByText('dynamicForm.uploadImage')).toBeTruthy() }) it('should render video upload field correctly', () => { const formSchema: FormSchema = { startNodes: [ { id: 'node_1', type: 'video', data: { label: '视频节点', }, }, ], } const { getByText } = render( ) expect(getByText('视频节点', { exact: false })).toBeTruthy() expect(getByText('dynamicForm.uploadVideo')).toBeTruthy() }) it('should render multiple fields in correct order', () => { const formSchema: FormSchema = { startNodes: [ { id: 'node_1', type: 'text', data: { label: '文本' }, }, { id: 'node_2', type: 'image', data: { label: '图片' }, }, { id: 'node_3', type: 'video', data: { label: '视频' }, }, ], } const { getByText } = render( ) expect(getByText('文本', { exact: false })).toBeTruthy() expect(getByText('图片', { exact: false })).toBeTruthy() expect(getByText('视频', { exact: false })).toBeTruthy() }) }) describe('Text Input Field', () => { it('should update text input value', () => { const formSchema: FormSchema = { startNodes: [ { id: 'node_1', type: 'text', data: { label: '文本' }, }, ], } const { getByTestId } = render( ) const input = getByTestId('text-input-node_1') fireEvent.changeText(input, 'Test text content') expect(input.props.value).toBe('Test text content') }) it('should show validation error for empty text field on submit', async () => { const formSchema: FormSchema = { startNodes: [ { id: 'node_1', type: 'text', data: { label: '文本' }, }, ], } const { getByTestId } = render( ) const submitButton = getByTestId('submit-button') fireEvent.press(submitButton) await waitFor(() => { expect(Toast.show).toHaveBeenCalledWith({ title: 'dynamicForm.fillRequiredFields', }) }) }) it('should clear error when user starts typing', async () => { const formSchema: FormSchema = { startNodes: [ { id: 'node_1', type: 'text', data: { label: '文本' }, }, ], } const { getByTestId, queryByText } = render( ) const submitButton = getByTestId('submit-button') fireEvent.press(submitButton) await waitFor(() => { expect(queryByText('文本dynamicForm.required')).toBeTruthy() }) const input = getByTestId('text-input-node_1') fireEvent.changeText(input, 'Some text') await waitFor(() => { expect(queryByText('文本dynamicForm.required')).toBeNull() }) }) }) describe('Image Upload Field', () => { it('should open drawer when image upload button is pressed', () => { const formSchema: FormSchema = { startNodes: [ { id: 'node_1', type: 'image', data: { label: '图片' }, }, ], } const { getByText } = render( ) const uploadButton = getByText('dynamicForm.uploadImage') fireEvent.press(uploadButton) // Drawer should be opened (state change) // This is handled internally by the component }) it('should show validation error for empty image field', async () => { const formSchema: FormSchema = { startNodes: [ { id: 'node_1', type: 'image', data: { label: '图片' }, }, ], } const { getByTestId } = render( ) const submitButton = getByTestId('submit-button') fireEvent.press(submitButton) await waitFor(() => { expect(Toast.show).toHaveBeenCalled() }) }) }) describe('Video Upload Field', () => { it('should open drawer when video upload button is pressed', () => { const formSchema: FormSchema = { startNodes: [ { id: 'node_1', type: 'video', data: { label: '视频' }, }, ], } const { getByText } = render( ) const uploadButton = getByText('dynamicForm.uploadVideo') fireEvent.press(uploadButton) }) }) describe('Form Submission', () => { it('should submit form data correctly when all fields are filled', async () => { const formSchema: FormSchema = { startNodes: [ { id: 'node_1', type: 'text', data: { label: '文本' }, }, { id: 'node_2', type: 'image', data: { label: '图片' }, }, ], } mockOnSubmit.mockResolvedValue({ generationId: 'gen_123' }) const { getByTestId, getByText } = render( ) // Fill text field const input = getByTestId('text-input-node_1') fireEvent.changeText(input, 'Test content') // Upload would be handled by drawer, for testing we simulate direct state update // In real scenario, user would go through the upload flow // Note: In actual test, we'd need to mock the drawer flow // For simplicity, this test shows the structure }) it('should show loading state during submission', () => { const formSchema: FormSchema = { startNodes: [ { id: 'node_1', type: 'text', data: { label: '文本' }, }, ], } const { getByTestId } = render( ) const submitButton = getByTestId('submit-button') expect(submitButton).toBeTruthy() }) it('should handle submission errors', async () => { const formSchema: FormSchema = { startNodes: [ { id: 'node_1', type: 'text', data: { label: '文本' }, }, ], } mockOnSubmit.mockResolvedValue({ error: { message: 'Submission failed' }, }) const { getByTestId } = render( ) const input = getByTestId('text-input-node_1') fireEvent.changeText(input, 'Test content') const submitButton = getByTestId('submit-button') fireEvent.press(submitButton) await waitFor(() => { expect(mockOnSubmit).toHaveBeenCalled() }) }) }) describe('Form State Management', () => { it('should initialize with default text value from node data', () => { const formSchema: FormSchema = { startNodes: [ { id: 'node_1', type: 'text', data: { label: '文本', text: 'Default text', }, }, ], } const { getByTestId } = render( ) const input = getByTestId('text-input-node_1') expect(input.props.value).toBe('Default text') }) it('should maintain separate state for multiple fields of same type', () => { const formSchema: FormSchema = { startNodes: [ { id: 'node_1', type: 'text', data: { label: '文本1' }, }, { id: 'node_2', type: 'text', data: { label: '文本2' }, }, ], } const { getAllByTestId } = render( ) const inputs = getAllByTestId(/text-input-node_/) expect(inputs.length).toBeGreaterThanOrEqual(2) fireEvent.changeText(inputs[0], 'First text') fireEvent.changeText(inputs[1], 'Second text') expect(inputs[0].props.value).toBe('First text') expect(inputs[1].props.value).toBe('Second text') }) }) describe('Edge Cases', () => { it('should handle undefined node data gracefully', () => { const formSchema: FormSchema = { startNodes: [ { id: 'node_1', type: 'text', }, ], } const { getByTestId } = render( ) expect(getByTestId('text-input-node_1')).toBeTruthy() }) it('should handle unknown node type gracefully', () => { const formSchema: FormSchema = { startNodes: [ { id: 'node_1', type: 'unknown' as any, data: { label: '未知类型' }, }, ], } const { queryByText } = render( ) // Unknown types should not render expect(queryByText('未知类型')).toBeNull() }) }) })