test(base): added test coverage to form components (#32436)

This commit is contained in:
akashseth-ifp 2026-02-24 20:00:35 +05:30 committed by GitHub
parent 00935fe526
commit 8761109a34
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 1405 additions and 19 deletions

View File

@ -0,0 +1,293 @@
import type { AnyFieldApi } from '@tanstack/react-form'
import type { FormSchema } from '@/app/components/base/form/types'
import { useForm } from '@tanstack/react-form'
import { fireEvent, render, screen } from '@testing-library/react'
import { FormItemValidateStatusEnum, FormTypeEnum } from '@/app/components/base/form/types'
import BaseField from './base-field'
const mockDynamicOptions = vi.fn()
vi.mock('@/hooks/use-i18n', () => ({
useRenderI18nObject: () => (content: Record<string, string>) => content.en_US ?? Object.values(content)[0] ?? '',
}))
vi.mock('@/service/use-triggers', () => ({
useTriggerPluginDynamicOptions: (...args: unknown[]) => mockDynamicOptions(...args),
}))
const renderBaseField = ({
formSchema,
defaultValues,
fieldState,
onChange,
showCurrentValue = false,
}: {
formSchema: FormSchema
defaultValues?: Record<string, unknown>
fieldState?: {
validateStatus?: FormItemValidateStatusEnum
errors?: string[]
warnings?: string[]
}
onChange?: (field: string, value: unknown) => void
showCurrentValue?: boolean
}) => {
const TestComponent = () => {
const form = useForm({
defaultValues: defaultValues ?? { [formSchema.name]: '' },
onSubmit: async () => {},
})
return (
<>
<form.Field name={formSchema.name}>
{field => (
<BaseField
field={field as unknown as AnyFieldApi}
formSchema={formSchema}
fieldState={fieldState}
onChange={onChange}
/>
)}
</form.Field>
{showCurrentValue && (
<form.Subscribe selector={state => state.values[formSchema.name]}>
{value => <div data-testid="field-value">{String(value)}</div>}
</form.Subscribe>
)}
</>
)
}
return render(<TestComponent />)
}
describe('BaseField', () => {
beforeEach(() => {
vi.clearAllMocks()
mockDynamicOptions.mockReturnValue({
data: undefined,
isLoading: false,
error: null,
})
})
it('should render text input and propagate changes', () => {
const onChange = vi.fn()
renderBaseField({
formSchema: {
type: FormTypeEnum.textInput,
name: 'title',
label: 'Title',
required: true,
},
defaultValues: { title: 'Hello' },
onChange,
})
const input = screen.getByDisplayValue('Hello')
expect(input).toHaveValue('Hello')
fireEvent.change(input, { target: { value: 'Updated' } })
expect(onChange).toHaveBeenCalledWith('title', 'Updated')
expect(screen.getByText('Title')).toBeInTheDocument()
expect(screen.getAllByText('*')).toHaveLength(1)
})
it('should render only options that satisfy show_on conditions', () => {
renderBaseField({
formSchema: {
type: FormTypeEnum.select,
name: 'mode',
label: 'Mode',
required: false,
options: [
{ label: 'Alpha', value: 'alpha' },
{ label: 'Beta', value: 'beta', show_on: [{ variable: 'enabled', value: 'yes' }] },
],
},
defaultValues: { mode: 'alpha', enabled: 'no' },
})
fireEvent.click(screen.getByText('Alpha'))
expect(screen.queryByText('Beta')).not.toBeInTheDocument()
})
it('should render dynamic select loading state', () => {
mockDynamicOptions.mockReturnValue({
data: undefined,
isLoading: true,
error: null,
})
renderBaseField({
formSchema: {
type: FormTypeEnum.dynamicSelect,
name: 'plugin',
label: 'Plugin',
required: false,
},
defaultValues: { plugin: '' },
})
expect(screen.getByText('common.dynamicSelect.loading')).toBeInTheDocument()
})
it('should update value when users click a radio option', () => {
const onChange = vi.fn()
renderBaseField({
formSchema: {
type: FormTypeEnum.radio,
name: 'visibility',
label: 'Visibility',
required: false,
options: [
{ label: 'Public', value: 'public' },
{ label: 'Private', value: 'private' },
],
},
defaultValues: { visibility: 'public' },
onChange,
})
fireEvent.click(screen.getByText('Private'))
expect(onChange).toHaveBeenCalledWith('visibility', 'private')
})
it('should show validation message when field state has an error', () => {
renderBaseField({
formSchema: {
type: FormTypeEnum.textInput,
name: 'name',
label: 'Name',
required: false,
},
fieldState: {
validateStatus: FormItemValidateStatusEnum.Error,
errors: ['Name is required'],
},
})
expect(screen.getByText('Name is required')).toBeInTheDocument()
})
it('should render description and help link when provided', () => {
renderBaseField({
formSchema: {
type: FormTypeEnum.textInput,
name: 'doc',
label: 'Documentation',
required: false,
description: 'Read the description',
url: 'https://example.com/help',
help: 'Open help docs',
},
defaultValues: { doc: '' },
})
expect(screen.getByText('Read the description')).toBeInTheDocument()
expect(screen.getByRole('link', { name: 'Open help docs' })).toHaveAttribute('href', 'https://example.com/help')
})
it('should render secret input with password type', () => {
renderBaseField({
formSchema: {
type: FormTypeEnum.secretInput,
name: 'token',
label: 'Token',
required: false,
},
defaultValues: { token: 'abc' },
})
expect(screen.getByDisplayValue('abc')).toHaveAttribute('type', 'password')
})
it('should render number input with number type', () => {
renderBaseField({
formSchema: {
type: FormTypeEnum.textNumber,
name: 'count',
label: 'Count',
required: false,
},
defaultValues: { count: 7 },
})
expect(screen.getByDisplayValue('7')).toHaveAttribute('type', 'number')
})
it('should render translated object label content', () => {
renderBaseField({
formSchema: {
type: FormTypeEnum.textInput,
name: 'title_i18n',
label: { en_US: 'Localized title', zh_Hans: '标题' },
required: false,
},
defaultValues: { title_i18n: '' },
})
expect(screen.getByText('Localized title')).toBeInTheDocument()
})
it('should render dynamic options and allow selecting one', () => {
mockDynamicOptions.mockReturnValue({
data: {
options: [
{ label: { en_US: 'Option A', zh_Hans: '选项A' }, value: 'a' },
],
},
isLoading: false,
error: null,
})
renderBaseField({
formSchema: {
type: FormTypeEnum.dynamicSelect,
name: 'plugin_option',
label: 'Plugin option',
required: false,
},
defaultValues: { plugin_option: '' },
})
fireEvent.click(screen.getByText('common.placeholder.input'))
fireEvent.click(screen.getByText('Option A'))
expect(screen.getByText('Option A')).toBeInTheDocument()
})
it('should update boolean field when users choose false', () => {
renderBaseField({
formSchema: {
type: FormTypeEnum.boolean,
name: 'enabled',
label: 'Enabled',
required: false,
},
defaultValues: { enabled: true },
showCurrentValue: true,
})
expect(screen.getByTestId('field-value')).toHaveTextContent('true')
fireEvent.click(screen.getByText('False'))
expect(screen.getByTestId('field-value')).toHaveTextContent('false')
})
it('should render warning message when field state has a warning', () => {
renderBaseField({
formSchema: {
type: FormTypeEnum.textInput,
name: 'warning_field',
label: 'Warning field',
required: false,
},
fieldState: {
validateStatus: FormItemValidateStatusEnum.Warning,
warnings: ['This is a warning'],
},
})
expect(screen.getByText('This is a warning')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,120 @@
import type { FormRefObject, FormSchema } from '@/app/components/base/form/types'
import { act, fireEvent, render, screen } from '@testing-library/react'
import { FormTypeEnum } from '@/app/components/base/form/types'
import BaseForm from './base-form'
vi.mock('@/service/use-triggers', () => ({
useTriggerPluginDynamicOptions: () => ({
data: undefined,
isLoading: false,
error: null,
}),
}))
const baseSchemas: FormSchema[] = [
{
type: FormTypeEnum.textInput,
name: 'kind',
label: 'Kind',
required: false,
default: 'show',
},
{
type: FormTypeEnum.textInput,
name: 'title',
label: 'Title',
required: true,
default: 'Initial title',
show_on: [{ variable: 'kind', value: 'show' }],
},
]
describe('BaseForm', () => {
it('should render nothing when no schemas are provided', () => {
const { container } = render(<BaseForm />)
expect(container.firstChild).toBeNull()
})
it('should render fields with default values from schema', () => {
render(<BaseForm formSchemas={baseSchemas} />)
expect(screen.getByDisplayValue('show')).toBeInTheDocument()
expect(screen.getByDisplayValue('Initial title')).toBeInTheDocument()
})
it('should hide conditional fields when show_on conditions are not met', () => {
render(
<BaseForm
formSchemas={baseSchemas}
defaultValues={{ kind: 'hide', title: 'Hidden title' }}
/>,
)
expect(screen.getByDisplayValue('hide')).toBeInTheDocument()
expect(screen.queryByDisplayValue('Hidden title')).not.toBeInTheDocument()
})
it('should prevent default submit behavior when preventDefaultSubmit is true', () => {
const onSubmit = vi.fn((event: React.FormEvent<HTMLFormElement>) => {
expect(event.defaultPrevented).toBe(true)
})
const { container } = render(
<BaseForm
formSchemas={baseSchemas}
onSubmit={onSubmit}
preventDefaultSubmit
/>,
)
fireEvent.submit(container.querySelector('form') as HTMLFormElement)
expect(onSubmit).toHaveBeenCalled()
})
it('should expose ref API for updating values and field states', () => {
const formRef = { current: null } as { current: FormRefObject | null }
render(
<BaseForm
formSchemas={baseSchemas}
ref={formRef}
/>,
)
expect(formRef.current).not.toBeNull()
act(() => {
formRef.current?.setFields([
{
name: 'title',
value: 'Changed title',
errors: ['Title is invalid'],
},
])
})
expect(screen.getByDisplayValue('Changed title')).toBeInTheDocument()
expect(screen.getByText('Title is invalid')).toBeInTheDocument()
expect(formRef.current?.getForm()).toBeTruthy()
expect(formRef.current?.getFormValues({})).toBeTruthy()
})
it('should derive warning status when setFields receives warnings only', () => {
const formRef = { current: null } as { current: FormRefObject | null }
render(
<BaseForm
formSchemas={baseSchemas}
ref={formRef}
/>,
)
act(() => {
formRef.current?.setFields([
{
name: 'title',
warnings: ['Title warning'],
},
])
})
expect(screen.getByText('Title warning')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,11 @@
import { BaseField, BaseForm } from '.'
describe('base component exports', () => {
it('should export BaseField', () => {
expect(BaseField).toBeDefined()
})
it('should export BaseForm', () => {
expect(BaseForm).toBeDefined()
})
})

View File

@ -0,0 +1,34 @@
import { fireEvent, render, screen } from '@testing-library/react'
import CheckboxField from './checkbox'
const mockField = {
name: 'checkbox-field',
state: {
value: false,
},
handleChange: vi.fn(),
}
vi.mock('../..', () => ({
useFieldContext: () => mockField,
}))
describe('CheckboxField', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should toggle on when unchecked users click the checkbox', () => {
mockField.state.value = false
render(<CheckboxField label="Enable feature" />)
fireEvent.click(screen.getByTestId('checkbox-checkbox-field'))
expect(mockField.handleChange).toHaveBeenCalledWith(true)
})
it('should toggle off when checked users click the label', () => {
mockField.state.value = true
render(<CheckboxField label="Enable feature" />)
fireEvent.click(screen.getByText('Enable feature'))
expect(mockField.handleChange).toHaveBeenCalledWith(false)
})
})

View File

@ -0,0 +1,49 @@
import { fireEvent, render, screen } from '@testing-library/react'
import CustomSelectField from './custom-select'
const mockField = {
name: 'custom-select-field',
state: {
value: 'small',
},
handleChange: vi.fn(),
}
vi.mock('../..', () => ({
useFieldContext: () => mockField,
}))
describe('CustomSelectField', () => {
beforeEach(() => {
vi.clearAllMocks()
mockField.state.value = 'small'
})
it('should render select placeholder or selected value', () => {
render(
<CustomSelectField
label="Size"
options={[
{ label: 'Small', value: 'small' },
{ label: 'Large', value: 'large' },
]}
/>,
)
expect(screen.getByText('Small')).toBeInTheDocument()
})
it('should update value when users select another option', () => {
render(
<CustomSelectField
label="Size"
options={[
{ label: 'Small', value: 'small' },
{ label: 'Large', value: 'large' },
]}
/>,
)
fireEvent.click(screen.getByText('Small'))
fireEvent.click(screen.getByText('Large'))
expect(mockField.handleChange).toHaveBeenCalledWith('large')
})
})

View File

@ -0,0 +1,127 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import FileTypesField from './file-types'
type FileTypeValue = {
allowedFileTypes: string[]
allowedFileExtensions: string[]
}
const mockField = {
name: 'allowed-types',
state: {
value: {
allowedFileTypes: [],
allowedFileExtensions: [],
} as FileTypeValue,
},
handleChange: vi.fn(),
}
vi.mock('../..', () => ({
useFieldContext: () => mockField,
}))
vi.mock('@/app/components/workflow/nodes/_base/components/file-type-item', () => ({
default: ({
type,
onToggle,
customFileTypes = [],
onCustomFileTypesChange,
}: {
type: SupportUploadFileTypes
onToggle: (type: SupportUploadFileTypes) => void
customFileTypes?: string[]
onCustomFileTypesChange?: (types: string[]) => void
}) => (
<div>
<button onClick={() => onToggle(type)}>{type}</button>
{onCustomFileTypesChange && (
<input
aria-label="custom file extensions"
value={customFileTypes.join(',')}
onChange={e => onCustomFileTypesChange(
e.target.value.split(',').map(v => v.trim()).filter(Boolean),
)}
/>
)}
</div>
),
}))
describe('FileTypesField', () => {
beforeEach(() => {
vi.clearAllMocks()
mockField.state.value = {
allowedFileTypes: [],
allowedFileExtensions: [],
}
})
it('should render the label and available type options', () => {
render(<FileTypesField label="Allowed file types" />)
expect(screen.getByText('Allowed file types')).toBeInTheDocument()
expect(screen.getByRole('button', { name: SupportUploadFileTypes.document })).toBeInTheDocument()
expect(screen.getByRole('button', { name: SupportUploadFileTypes.image })).toBeInTheDocument()
expect(screen.getByRole('button', { name: SupportUploadFileTypes.audio })).toBeInTheDocument()
expect(screen.getByRole('button', { name: SupportUploadFileTypes.video })).toBeInTheDocument()
expect(screen.getByRole('button', { name: SupportUploadFileTypes.custom })).toBeInTheDocument()
})
it('should keep only custom when users choose custom types', () => {
mockField.state.value.allowedFileTypes = [SupportUploadFileTypes.document]
render(<FileTypesField label="Allowed file types" />)
fireEvent.click(screen.getByRole('button', { name: SupportUploadFileTypes.custom }))
expect(mockField.handleChange).toHaveBeenCalledWith({
allowedFileTypes: [SupportUploadFileTypes.custom],
allowedFileExtensions: [],
})
})
it('should remove custom and add selected standard type', () => {
mockField.state.value.allowedFileTypes = [SupportUploadFileTypes.custom]
render(<FileTypesField label="Allowed file types" />)
fireEvent.click(screen.getByRole('button', { name: SupportUploadFileTypes.image }))
expect(mockField.handleChange).toHaveBeenCalledWith({
allowedFileTypes: [SupportUploadFileTypes.image],
allowedFileExtensions: [],
})
})
it('should remove custom when users click custom again', () => {
mockField.state.value.allowedFileTypes = [SupportUploadFileTypes.custom]
render(<FileTypesField label="Allowed file types" />)
fireEvent.click(screen.getByRole('button', { name: SupportUploadFileTypes.custom }))
expect(mockField.handleChange).toHaveBeenCalledWith({
allowedFileTypes: [],
allowedFileExtensions: [],
})
})
it('should remove a selected standard type when users click it again', () => {
mockField.state.value.allowedFileTypes = [SupportUploadFileTypes.image]
render(<FileTypesField label="Allowed file types" />)
fireEvent.click(screen.getByRole('button', { name: SupportUploadFileTypes.image }))
expect(mockField.handleChange).toHaveBeenCalledWith({
allowedFileTypes: [],
allowedFileExtensions: [],
})
})
it('should update custom extensions when users type custom extension values', () => {
render(<FileTypesField label="Allowed file types" />)
fireEvent.change(screen.getByRole('textbox', { name: 'custom file extensions' }), {
target: { value: 'csv,pdf' },
})
expect(mockField.handleChange).toHaveBeenCalledWith({
allowedFileTypes: [],
allowedFileExtensions: ['csv', 'pdf'],
})
})
})

View File

@ -0,0 +1,82 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
import FileUploaderField from './file-uploader'
const mockField = {
name: 'files',
state: {
value: [
{
id: 'file-1',
name: 'report.pdf',
size: 1024,
type: 'application/pdf',
progress: 100,
transferMethod: TransferMethod.local_file,
supportFileType: SupportUploadFileTypes.document,
uploadedId: 'uploaded-1',
url: 'https://example.com/report.pdf',
},
],
},
handleChange: vi.fn(),
}
vi.mock('../..', () => ({
useFieldContext: () => mockField,
}))
vi.mock('next/navigation', () => ({
useParams: () => ({ token: 'test-token' }),
}))
describe('FileUploaderField', () => {
beforeEach(() => {
vi.clearAllMocks()
mockField.state.value = [
{
id: 'file-1',
name: 'report.pdf',
size: 1024,
type: 'application/pdf',
progress: 100,
transferMethod: TransferMethod.local_file,
supportFileType: SupportUploadFileTypes.document,
uploadedId: 'uploaded-1',
url: 'https://example.com/report.pdf',
},
]
})
it('should render existing uploaded file name', () => {
render(
<FileUploaderField
label="Attachments"
fileConfig={{
allowed_file_upload_methods: [TransferMethod.local_file],
allowed_file_types: [SupportUploadFileTypes.document],
}}
/>,
)
expect(screen.getByText('Attachments')).toBeInTheDocument()
expect(screen.getByText('report.pdf')).toBeInTheDocument()
})
it('should update field value when users remove a file', () => {
render(
<FileUploaderField
label="Attachments"
fileConfig={{
allowed_file_upload_methods: [TransferMethod.local_file],
allowed_file_types: [SupportUploadFileTypes.document],
}}
/>,
)
const deleteButtons = screen.getAllByRole('button')
fireEvent.click(deleteButtons[1])
expect(mockField.handleChange).toHaveBeenCalledWith([])
})
})

View File

@ -0,0 +1,20 @@
import { renderHook } from '@testing-library/react'
import { useInputTypeOptions } from './hooks'
describe('useInputTypeOptions', () => {
it('should include file options when supportFile is true', () => {
const { result } = renderHook(() => useInputTypeOptions(true))
const values = result.current.map(item => item.value)
expect(values).toContain('file')
expect(values).toContain('file-list')
})
it('should exclude file options when supportFile is false', () => {
const { result } = renderHook(() => useInputTypeOptions(false))
const values = result.current.map(item => item.value)
expect(values).not.toContain('file')
expect(values).not.toContain('file-list')
})
})

View File

@ -0,0 +1,37 @@
import { fireEvent, render, screen } from '@testing-library/react'
import InputTypeSelectField from './index'
const mockField = {
name: 'input-type',
state: {
value: 'text-input',
},
handleChange: vi.fn(),
}
vi.mock('../../..', () => ({
useFieldContext: () => mockField,
}))
describe('InputTypeSelectField', () => {
beforeEach(() => {
vi.clearAllMocks()
mockField.state.value = 'text-input'
})
it('should render label and selected option', () => {
render(<InputTypeSelectField label="Input type" supportFile={true} />)
expect(screen.getByText('Input type')).toBeInTheDocument()
expect(screen.getByText('appDebug.variableConfig.text-input')).toBeInTheDocument()
})
it('should update value when users choose another input type', () => {
render(<InputTypeSelectField label="Input type" supportFile={true} />)
fireEvent.click(screen.getByText('appDebug.variableConfig.text-input'))
fireEvent.click(screen.getByText('appDebug.variableConfig.number'))
expect(mockField.handleChange).toHaveBeenCalledWith('number')
})
})

View File

@ -0,0 +1,22 @@
import { render, screen } from '@testing-library/react'
import Option from './option'
const MockIcon = () => <svg aria-label="mock icon" />
describe('InputTypeSelect Option', () => {
it('should render option label and type', () => {
render(
<Option
option={{
value: 'checkbox',
label: 'Checkbox',
Icon: MockIcon,
type: 'boolean',
}}
/>,
)
expect(screen.getByText('Checkbox')).toBeInTheDocument()
expect(screen.getByText('boolean')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,28 @@
import { render, screen } from '@testing-library/react'
import Trigger from './trigger'
const MockIcon = () => <svg aria-label="mock icon" />
describe('InputTypeSelect Trigger', () => {
it('should show placeholder text when no option is selected', () => {
render(<Trigger option={undefined} open={false} />)
expect(screen.getByText('common.placeholder.select')).toBeInTheDocument()
})
it('should show selected option label and type', () => {
render(
<Trigger
option={{
value: 'text-input',
label: 'Text Input',
Icon: MockIcon,
type: 'string',
}}
open={false}
/>,
)
expect(screen.getByText('Text Input')).toBeInTheDocument()
expect(screen.getByText('string')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,12 @@
import { InputTypeEnum } from './types'
describe('InputTypeEnum', () => {
it('should accept valid input types', () => {
expect(InputTypeEnum.parse('text-input')).toBe('text-input')
expect(InputTypeEnum.parse('file-list')).toBe('file-list')
})
it('should reject invalid input types', () => {
expect(() => InputTypeEnum.parse('invalid-type')).toThrow()
})
})

View File

@ -0,0 +1,17 @@
import { render, screen } from '@testing-library/react'
import MixedVariableTextInput from './index'
describe('MixedVariableTextInput', () => {
it('should render placeholder guidance and data type badge', () => {
render(<MixedVariableTextInput />)
expect(screen.getByText('Type or press')).toBeInTheDocument()
expect(screen.getByText('insert variable')).toBeInTheDocument()
expect(screen.getByText('String')).toBeInTheDocument()
})
it('should keep placeholder visible when editor is not editable', () => {
render(<MixedVariableTextInput editable={false} />)
expect(screen.getByText('insert variable')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,74 @@
import type { EditorState } from 'lexical'
import { LexicalComposer } from '@lexical/react/LexicalComposer'
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { $getRoot } from 'lexical'
import { CustomTextNode } from '@/app/components/base/prompt-editor/plugins/custom-text/node'
import Placeholder from './placeholder'
const config = {
namespace: 'placeholder-test',
theme: {},
nodes: [CustomTextNode],
onError: (error: Error) => {
throw error
},
}
describe('MixedVariable Placeholder', () => {
it('should render helper text and insert variable action', () => {
render(
<LexicalComposer initialConfig={config}>
<Placeholder />
</LexicalComposer>,
)
expect(screen.getByText('Type or press')).toBeInTheDocument()
expect(screen.getByText('insert variable')).toBeInTheDocument()
expect(screen.getByText('String')).toBeInTheDocument()
})
it('should render shortcut symbol for variable insertion', () => {
render(
<LexicalComposer initialConfig={config}>
<Placeholder />
</LexicalComposer>,
)
expect(screen.getByText('/')).toBeInTheDocument()
})
it('should insert text and keep editor content available after click', async () => {
const user = userEvent.setup()
let editorText = ''
const handleChange = (editorState: EditorState) => {
editorState.read(() => {
editorText = $getRoot().getTextContent()
})
}
render(
<LexicalComposer initialConfig={config}>
<OnChangePlugin onChange={handleChange} />
<Placeholder />
</LexicalComposer>,
)
await user.click(screen.getByText('insert variable'))
expect(editorText).toContain('/')
})
it('should handle container click without breaking the helper UI', async () => {
const user = userEvent.setup()
render(
<LexicalComposer initialConfig={config}>
<Placeholder />
</LexicalComposer>,
)
await user.click(screen.getByText('Type or press'))
expect(screen.getByText('insert variable')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,33 @@
import { fireEvent, render, screen } from '@testing-library/react'
import NumberInputField from './number-input'
const mockField = {
name: 'number-field',
state: {
value: 2,
},
handleChange: vi.fn(),
handleBlur: vi.fn(),
}
vi.mock('../..', () => ({
useFieldContext: () => mockField,
}))
describe('NumberInputField', () => {
beforeEach(() => {
vi.clearAllMocks()
mockField.state.value = 2
})
it('should render current number value', () => {
render(<NumberInputField label="Count" />)
expect(screen.getByDisplayValue('2')).toBeInTheDocument()
})
it('should update value when users click increment', () => {
render(<NumberInputField label="Count" />)
fireEvent.click(screen.getByRole('button', { name: 'increment' }))
expect(mockField.handleChange).toHaveBeenCalledWith(3)
})
})

View File

@ -0,0 +1,46 @@
import { fireEvent, render, screen } from '@testing-library/react'
import NumberSliderField from './number-slider'
const mockField = {
name: 'slider-field',
state: {
value: 2,
},
handleChange: vi.fn(),
}
vi.mock('../..', () => ({
useFieldContext: () => mockField,
}))
vi.mock('@/app/components/workflow/nodes/_base/components/input-number-with-slider', () => ({
default: ({
value,
onChange,
}: {
value: number
onChange: (value: number) => void
}) => (
<button onClick={() => onChange(value + 1)}>
{`slider-value-${value}`}
</button>
),
}))
describe('NumberSliderField', () => {
beforeEach(() => {
vi.clearAllMocks()
mockField.state.value = 2
})
it('should render description when provided', () => {
render(<NumberSliderField label="Threshold" description="Used to control threshold" />)
expect(screen.getByText('Used to control threshold')).toBeInTheDocument()
})
it('should update value when users interact with slider', () => {
render(<NumberSliderField label="Threshold" />)
fireEvent.click(screen.getByRole('button', { name: 'slider-value-2' }))
expect(mockField.handleChange).toHaveBeenCalledWith(3)
})
})

View File

@ -0,0 +1,45 @@
import { fireEvent, render, screen } from '@testing-library/react'
import OptionsField from './options'
const mockField = {
name: 'options-field',
state: {
value: [] as { label: string, value: string }[],
},
handleChange: vi.fn(),
}
vi.mock('../..', () => ({
useFieldContext: () => mockField,
}))
vi.mock('@/app/components/app/configuration/config-var/config-select', () => ({
default: ({
onChange,
}: {
onChange: (value: { label: string, value: string }[]) => void
}) => (
<button onClick={() => onChange([{ label: 'A', value: 'a' }])}>
apply-options
</button>
),
}))
describe('OptionsField', () => {
beforeEach(() => {
vi.clearAllMocks()
mockField.state.value = []
})
it('should render label and options control', () => {
render(<OptionsField label="Allowed options" />)
expect(screen.getByText('Allowed options')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'apply-options' })).toBeInTheDocument()
})
it('should update options when users apply changes', () => {
render(<OptionsField label="Allowed options" />)
fireEvent.click(screen.getByRole('button', { name: 'apply-options' }))
expect(mockField.handleChange).toHaveBeenCalledWith([{ label: 'A', value: 'a' }])
})
})

View File

@ -0,0 +1,49 @@
import { fireEvent, render, screen } from '@testing-library/react'
import SelectField from './select'
const mockField = {
name: 'select-field',
state: {
value: 'alpha',
},
handleChange: vi.fn(),
}
vi.mock('../..', () => ({
useFieldContext: () => mockField,
}))
describe('SelectField', () => {
beforeEach(() => {
vi.clearAllMocks()
mockField.state.value = 'alpha'
})
it('should render selected value', () => {
render(
<SelectField
label="Mode"
options={[
{ label: 'Alpha', value: 'alpha' },
{ label: 'Beta', value: 'beta' },
]}
/>,
)
expect(screen.getByText('Alpha')).toBeInTheDocument()
})
it('should update value when users select another option', () => {
render(
<SelectField
label="Mode"
options={[
{ label: 'Alpha', value: 'alpha' },
{ label: 'Beta', value: 'beta' },
]}
/>,
)
fireEvent.click(screen.getByText('Alpha'))
fireEvent.click(screen.getByText('Beta'))
expect(mockField.handleChange).toHaveBeenCalledWith('beta')
})
})

View File

@ -0,0 +1,33 @@
import { fireEvent, render, screen } from '@testing-library/react'
import TextAreaField from './text-area'
const mockField = {
name: 'text-area-field',
state: {
value: 'Initial note',
},
handleChange: vi.fn(),
handleBlur: vi.fn(),
}
vi.mock('../..', () => ({
useFieldContext: () => mockField,
}))
describe('TextAreaField', () => {
beforeEach(() => {
vi.clearAllMocks()
mockField.state.value = 'Initial note'
})
it('should render current value', () => {
render(<TextAreaField label="Note" />)
expect(screen.getByLabelText('Note')).toHaveValue('Initial note')
})
it('should update value when users type', () => {
render(<TextAreaField label="Note" />)
fireEvent.change(screen.getByLabelText('Note'), { target: { value: 'Updated note' } })
expect(mockField.handleChange).toHaveBeenCalledWith('Updated note')
})
})

View File

@ -0,0 +1,33 @@
import { fireEvent, render, screen } from '@testing-library/react'
import TextField from './text'
const mockField = {
name: 'text-field',
state: {
value: 'Initial text',
},
handleChange: vi.fn(),
handleBlur: vi.fn(),
}
vi.mock('../..', () => ({
useFieldContext: () => mockField,
}))
describe('TextField', () => {
beforeEach(() => {
vi.clearAllMocks()
mockField.state.value = 'Initial text'
})
it('should render current value', () => {
render(<TextField label="Name" />)
expect(screen.getByLabelText('Name')).toHaveValue('Initial text')
})
it('should update value when users type', () => {
render(<TextField label="Name" />)
fireEvent.change(screen.getByLabelText('Name'), { target: { value: 'Updated text' } })
expect(mockField.handleChange).toHaveBeenCalledWith('Updated text')
})
})

View File

@ -0,0 +1,64 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { TransferMethod } from '@/types/app'
import UploadMethodField from './upload-method'
const mockField = {
name: 'upload-method',
state: {
value: [TransferMethod.local_file] as TransferMethod[],
},
handleChange: vi.fn(),
}
vi.mock('../..', () => ({
useFieldContext: () => mockField,
}))
vi.mock('@/app/components/workflow/nodes/_base/components/option-card', () => ({
default: ({
title,
selected,
onSelect,
}: {
title: string
selected: boolean
onSelect: () => void
}) => (
<button aria-pressed={selected} onClick={onSelect}>
{title}
</button>
),
}))
describe('UploadMethodField', () => {
beforeEach(() => {
vi.clearAllMocks()
mockField.state.value = [TransferMethod.local_file]
})
it('should show all upload method options', () => {
render(<UploadMethodField label="Upload methods" />)
expect(screen.getByText('Upload methods')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'appDebug.variableConfig.localUpload' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'URL' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'appDebug.variableConfig.both' })).toBeInTheDocument()
})
it('should switch to URL-only when users select URL', () => {
render(<UploadMethodField label="Upload methods" />)
fireEvent.click(screen.getByRole('button', { name: 'URL' }))
expect(mockField.handleChange).toHaveBeenCalledWith([TransferMethod.remote_url])
})
it('should enable both methods when users select both', () => {
render(<UploadMethodField label="Upload methods" />)
fireEvent.click(screen.getByRole('button', { name: 'appDebug.variableConfig.both' }))
expect(mockField.handleChange).toHaveBeenCalledWith([
TransferMethod.local_file,
TransferMethod.remote_url,
])
})
})

View File

@ -0,0 +1,47 @@
import { fireEvent, render, screen } from '@testing-library/react'
import VariableOrConstantInputField from './variable-or-constant-input'
vi.mock('@/app/components/workflow/nodes/_base/components/variable/var-reference-picker', () => ({
default: ({ onChange }: { onChange?: () => void }) => (
<button onClick={() => onChange?.()}>
Variable picker
</button>
),
}))
describe('VariableOrConstantInputField', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should render variable picker by default', () => {
render(<VariableOrConstantInputField label="Input source" />)
expect(screen.getByRole('button', { name: 'Variable picker' })).toBeInTheDocument()
})
it('should switch to constant input when users choose constant', () => {
render(<VariableOrConstantInputField label="Input source" />)
fireEvent.click(screen.getAllByRole('button')[1])
expect(screen.queryByRole('button', { name: 'Variable picker' })).not.toBeInTheDocument()
expect(screen.getByRole('textbox')).toBeInTheDocument()
})
it('should show typed constant value in the input', () => {
render(<VariableOrConstantInputField label="Input source" />)
fireEvent.click(screen.getAllByRole('button')[1])
const textbox = screen.getByRole('textbox')
fireEvent.change(textbox, { target: { value: 'constant-value' } })
expect(textbox).toHaveValue('constant-value')
})
it('should switch back to variable mode when users choose variable again', () => {
render(<VariableOrConstantInputField label="Input source" />)
const modeButtons = screen.getAllByRole('button')
fireEvent.click(modeButtons[1])
expect(screen.getByRole('textbox')).toBeInTheDocument()
fireEvent.click(modeButtons[0])
expect(screen.getByRole('button', { name: 'Variable picker' })).toBeInTheDocument()
})
})

View File

@ -0,0 +1,29 @@
import { fireEvent, render, screen } from '@testing-library/react'
import VariableSelectorField from './variable-selector'
vi.mock('@/app/components/workflow/nodes/_base/components/variable/var-reference-picker', () => ({
default: ({ onChange }: { onChange?: () => void }) => (
<button onClick={() => onChange?.()}>
Variable picker
</button>
),
}))
describe('VariableSelectorField', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should render label and variable picker', () => {
render(<VariableSelectorField label="Reference variable" />)
expect(screen.getByText('Reference variable')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Variable picker' })).toBeInTheDocument()
})
it('should keep picker available after users pick a variable', () => {
render(<VariableSelectorField label="Reference variable" />)
const pickerButton = screen.getByRole('button', { name: 'Variable picker' })
fireEvent.click(pickerButton)
expect(pickerButton).toBeInTheDocument()
})
})

View File

@ -0,0 +1,75 @@
import type { FormType } from '../..'
import type { CustomActionsProps } from './actions'
import { fireEvent, render, screen } from '@testing-library/react'
import { formContext } from '../..'
import Actions from './actions'
const renderWithForm = ({
canSubmit,
isSubmitting,
CustomActions,
}: {
canSubmit: boolean
isSubmitting: boolean
CustomActions?: (props: CustomActionsProps) => React.ReactNode
}) => {
const submitSpy = vi.fn()
const state = {
canSubmit,
isSubmitting,
}
const form = {
store: {
state,
subscribe: () => () => {},
},
handleSubmit: submitSpy,
}
const TestComponent = () => {
return (
<formContext.Provider value={form as unknown as FormType}>
<Actions
CustomActions={CustomActions}
/>
</formContext.Provider>
)
}
render(<TestComponent />)
return { submitSpy }
}
describe('Actions', () => {
it('should disable submit button when form cannot submit', () => {
renderWithForm({ canSubmit: false, isSubmitting: false })
expect(screen.getByRole('button', { name: 'common.operation.submit' })).toBeDisabled()
})
it('should call form submit when users click submit button', () => {
const { submitSpy } = renderWithForm({ canSubmit: true, isSubmitting: false })
fireEvent.click(screen.getByRole('button', { name: 'common.operation.submit' }))
expect(submitSpy).toHaveBeenCalledTimes(1)
})
it('should render custom actions when provided', () => {
const customActionsSpy = vi.fn(({ isSubmitting, canSubmit }: CustomActionsProps) => (
<div>
{`custom-${String(isSubmitting)}-${String(canSubmit)}`}
</div>
))
renderWithForm({
canSubmit: true,
isSubmitting: true,
CustomActions: customActionsSpy,
})
expect(screen.queryByRole('button', { name: 'common.operation.submit' })).not.toBeInTheDocument()
expect(screen.getByText('custom-true-true')).toBeInTheDocument()
expect(customActionsSpy).toHaveBeenCalledWith(expect.objectContaining({
isSubmitting: true,
canSubmit: true,
}))
})
})

View File

@ -1,45 +1,51 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Label from './label'
describe('Label Component', () => {
describe('Label', () => {
const defaultProps = {
htmlFor: 'test-input',
label: 'Test Label',
}
it('renders basic label correctly', () => {
it('should render the label text', () => {
render(<Label {...defaultProps} />)
const label = screen.getByTestId('label')
expect(label).toBeInTheDocument()
expect(label).toHaveAttribute('for', 'test-input')
expect(screen.getByText('Test Label')).toBeInTheDocument()
})
it('shows optional text when showOptional is true', () => {
it('should focus related input when users click the label', async () => {
const user = userEvent.setup()
render(
<>
<Label {...defaultProps} />
<input id="test-input" />
</>,
)
await user.click(screen.getByText('Test Label'))
expect(screen.getByRole('textbox')).toHaveFocus()
})
it('should show optional text when the field is not required', () => {
render(<Label {...defaultProps} showOptional />)
expect(screen.getByText('common.label.optional')).toBeInTheDocument()
})
it('shows required asterisk when isRequired is true', () => {
it('should show required marker when the field is required', () => {
render(<Label {...defaultProps} isRequired />)
expect(screen.getByText('*')).toBeInTheDocument()
})
it('renders tooltip when tooltip prop is provided', () => {
it('should show tooltip content on hover', async () => {
const user = userEvent.setup()
const tooltipText = 'Test Tooltip'
render(<Label {...defaultProps} tooltip={tooltipText} />)
const trigger = screen.getByTestId('test-input-tooltip')
fireEvent.mouseEnter(trigger)
await user.hover(screen.getByTestId('test-input-tooltip'))
expect(screen.getByText(tooltipText)).toBeInTheDocument()
})
it('applies custom className when provided', () => {
const customClass = 'custom-label'
render(<Label {...defaultProps} className={customClass} />)
const label = screen.getByTestId('label')
expect(label).toHaveClass(customClass)
})
it('does not show optional text and required asterisk simultaneously', () => {
it('should hide optional text when required is true', () => {
render(<Label {...defaultProps} isRequired showOptional />)
expect(screen.queryByText('common.label.optional')).not.toBeInTheDocument()
expect(screen.getByText('*')).toBeInTheDocument()