mirror of
https://github.com/langgenius/dify.git
synced 2026-03-10 11:10:19 +08:00
test(base): added test coverage to form components (#32436)
This commit is contained in:
parent
00935fe526
commit
8761109a34
293
web/app/components/base/form/components/base/base-field.spec.tsx
Normal file
293
web/app/components/base/form/components/base/base-field.spec.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
120
web/app/components/base/form/components/base/base-form.spec.tsx
Normal file
120
web/app/components/base/form/components/base/base-form.spec.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
11
web/app/components/base/form/components/base/index.spec.tsx
Normal file
11
web/app/components/base/form/components/base/index.spec.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
@ -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'],
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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([])
|
||||
})
|
||||
})
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
@ -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' }])
|
||||
})
|
||||
})
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
33
web/app/components/base/form/components/field/text.spec.tsx
Normal file
33
web/app/components/base/form/components/field/text.spec.tsx
Normal 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')
|
||||
})
|
||||
})
|
||||
@ -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,
|
||||
])
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
@ -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,
|
||||
}))
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user