test(workflow-app): enhance unit tests for workflow components and hooks (#34065)

Co-authored-by: CodingOnStar <hanxujiang@dify.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: lif <1835304752@qq.com>
Co-authored-by: hjlarry <hjlarry@163.com>
Co-authored-by: Stephen Zhou <hi@hyoban.cc>
Co-authored-by: tmimmanuel <14046872+tmimmanuel@users.noreply.github.com>
Co-authored-by: Desel72 <pedroluiscolmenares722@gmail.com>
Co-authored-by: Renzo <170978465+RenzoMXD@users.noreply.github.com>
Co-authored-by: Krishna Chaitanya <krishnabkc15@gmail.com>
Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Coding On Star
2026-03-25 18:34:32 +08:00
committed by GitHub
parent 0e6d97acf9
commit 449d8c7768
40 changed files with 5608 additions and 796 deletions

View File

@ -90,6 +90,22 @@ describe('useVariableModalState', () => {
])
})
it('should keep valid object rows when switching to json mode from form mode', () => {
const { result } = renderHook(() => useVariableModalState(createOptions()))
act(() => {
result.current.handleTypeChange(ChatVarType.Object)
result.current.setObjectValue([
{ key: '', type: ChatVarType.String, value: undefined },
{ key: 'timeout', type: ChatVarType.Number, value: 30 },
])
result.current.handleEditorChange(true)
})
expect(result.current.editInJSON).toBe(true)
expect(result.current.value).toEqual({ timeout: 30 })
expect(result.current.editorContent).toBe(JSON.stringify({ timeout: 30 }))
})
it('should reset object form values when leaving empty json mode', () => {
const { result } = renderHook(() => useVariableModalState(createOptions({
chatVar: {
@ -141,6 +157,19 @@ describe('useVariableModalState', () => {
expect(result.current.editorContent).toBe(JSON.stringify(['True', 'False']))
})
it('should preserve zero values when switching number arrays into json mode', () => {
const { result } = renderHook(() => useVariableModalState(createOptions()))
act(() => {
result.current.handleTypeChange(ChatVarType.ArrayNumber)
result.current.setValue([0, 2, undefined])
result.current.handleEditorChange(true)
})
expect(result.current.editInJSON).toBe(true)
expect(result.current.value).toEqual([0, 2])
expect(result.current.editorContent).toBe(JSON.stringify([0, 2]))
})
it('should notify and stop saving when object keys are invalid', () => {
const notify = vi.fn()
const onSave = vi.fn()
@ -161,7 +190,7 @@ describe('useVariableModalState', () => {
result.current.handleSave()
})
expect(notify).toHaveBeenCalledWith({ type: 'error', message: 'object key can not be empty' })
expect(notify).toHaveBeenCalledWith({ type: 'error', message: 'chatVariable.modal.objectKeyRequired' })
expect(onSave).not.toHaveBeenCalled()
expect(onClose).not.toHaveBeenCalled()
})

View File

@ -33,6 +33,10 @@ describe('variable-modal helpers', () => {
{ key: '', type: ChatVarType.Number, value: 1 },
])).toEqual({ apiKey: 'secret' })
expect(formatObjectValueFromList([
{ key: 'count', type: ChatVarType.Number, value: 0 },
{ key: 'label', type: ChatVarType.String, value: '' },
])).toEqual({ count: 0, label: null })
expect(formatChatVariableValue({
editInJSON: false,
objectValue: [{ key: 'enabled', type: ChatVarType.String, value: 'true' }],
@ -54,6 +58,13 @@ describe('variable-modal helpers', () => {
value: ['a', '', 'b'],
})).toEqual(['a', 'b'])
expect(formatChatVariableValue({
editInJSON: false,
objectValue: [],
type: ChatVarType.ArrayNumber,
value: [0, 1, undefined, null, ''] as unknown as Array<number | undefined>,
})).toEqual([0, 1])
expect(formatChatVariableValue({
editInJSON: false,
objectValue: [],
@ -94,6 +105,10 @@ describe('variable-modal helpers', () => {
type: ChatVarType.ArrayBoolean,
})).toEqual([true, false, true, false])
expect(() => parseEditorContent({
content: '{"enabled":true}',
type: ChatVarType.ArrayBoolean,
})).toThrow('JSON array')
expect(parseEditorContent({
content: '{"enabled":true}',
type: ChatVarType.Object,

View File

@ -80,7 +80,7 @@ describe('variable-modal', () => {
await user.type(screen.getByPlaceholderText('workflow.chatVariable.modal.namePlaceholder'), 'existing_name')
await user.click(screen.getByText('common.operation.save'))
expect(mockToastError.mock.calls.at(-1)?.[0]).toBe('name is existed')
expect(mockToastError.mock.calls.at(-1)?.[0]).toBe('appDebug.varKeyError.keyAlreadyExists:{"key":"workflow.chatVariable.modal.name"}')
expect(onSave).not.toHaveBeenCalled()
})
@ -100,8 +100,10 @@ describe('variable-modal', () => {
expect(screen.getByDisplayValue('secret')).toBeInTheDocument()
expect(screen.getByDisplayValue('30')).toBeInTheDocument()
const timeoutInput = screen.getByDisplayValue('30') as HTMLInputElement
await user.clear(screen.getByDisplayValue('secret'))
await user.type(screen.getByDisplayValue('30'), '5')
await user.clear(timeoutInput)
await user.type(timeoutInput, '5')
await user.click(screen.getByText('common.operation.save'))
expect(onSave).toHaveBeenCalledWith({
@ -110,7 +112,7 @@ describe('variable-modal', () => {
value_type: ChatVarType.Object,
value: {
apiKey: null,
timeout: 305,
timeout: 5,
},
description: 'settings',
})
@ -195,4 +197,22 @@ describe('variable-modal', () => {
description: '',
})
})
it('should keep the number input empty while editing after the user clears it', async () => {
const user = userEvent.setup()
renderVariableModal({
chatVar: {
id: 'var-4',
name: 'timeout',
description: '',
value_type: ChatVarType.Number,
value: 3,
},
})
const input = screen.getByDisplayValue('3') as HTMLInputElement
await user.clear(input)
expect(input.value).toBe('')
})
})

View File

@ -108,7 +108,7 @@ export const useVariableModalState = ({
if (prev.type === ChatVarType.Object) {
if (nextEditInJSON) {
const nextValue = !prev.objectValue[0].key ? undefined : formatObjectValueFromList(prev.objectValue)
const nextValue = prev.objectValue.some(item => item.key) ? formatObjectValueFromList(prev.objectValue) : undefined
nextState.value = nextValue
nextState.editorContent = JSON.stringify(nextValue)
return nextState
@ -133,8 +133,11 @@ export const useVariableModalState = ({
if (prev.type === ChatVarType.ArrayString || prev.type === ChatVarType.ArrayNumber) {
if (nextEditInJSON) {
const nextValue = (Array.isArray(prev.value) && prev.value.length && prev.value.filter(Boolean).length)
? prev.value.filter(Boolean)
const compactValues = Array.isArray(prev.value)
? prev.value.filter(item => item !== null && item !== undefined && item !== '')
: []
const nextValue = compactValues.length
? compactValues
: undefined
nextState.value = nextValue
if (!prev.editorContent)
@ -181,12 +184,15 @@ export const useVariableModalState = ({
return
if (!chatVar && conversationVariables.some(item => item.name === state.name)) {
notify({ type: 'error', message: 'name is existed' })
notify({
type: 'error',
message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: t('chatVariable.modal.name', { ns: 'workflow' }) }),
})
return
}
if (state.type === ChatVarType.Object && state.objectValue.some(item => !item.key && !!item.value)) {
notify({ type: 'error', message: 'object key can not be empty' })
if (state.type === ChatVarType.Object && state.objectValue.some(item => !item.key && item.value !== undefined && item.value !== '')) {
notify({ type: 'error', message: t('chatVariable.modal.objectKeyRequired', { ns: 'workflow' }) })
return
}

View File

@ -72,7 +72,7 @@ export const buildObjectValueItems = (chatVar?: ConversationVariable): ObjectVal
export const formatObjectValueFromList = (list: ObjectValueItem[]) => {
return list.reduce<Record<string, string | number | null>>((acc, curr) => {
if (curr.key)
acc[curr.key] = curr.value || null
acc[curr.key] = curr.value === '' || curr.value === undefined ? null : curr.value
return acc
}, {})
}
@ -88,6 +88,8 @@ export const formatChatVariableValue = ({
type: ChatVarType
value: unknown
}) => {
const compactArrayValue = (items: unknown[]) =>
items.filter(item => item !== null && item !== undefined && item !== '')
switch (type) {
case ChatVarTypeEnum.String:
return value || ''
@ -100,7 +102,7 @@ export const formatChatVariableValue = ({
case ChatVarTypeEnum.ArrayString:
case ChatVarTypeEnum.ArrayNumber:
case ChatVarTypeEnum.ArrayObject:
return Array.isArray(value) ? value.filter(Boolean) : []
return Array.isArray(value) ? compactArrayValue(value) : []
case ChatVarTypeEnum.ArrayBoolean:
return value || []
}
@ -151,6 +153,8 @@ export const parseEditorContent = ({
if (type !== ChatVarTypeEnum.ArrayBoolean)
return parsed
if (!Array.isArray(parsed))
throw new TypeError('ArrayBoolean editor content must be a JSON array')
return parsed
.map((item: string | boolean) => {
if (item === 'True' || item === 'true' || item === true)

View File

@ -138,7 +138,10 @@ export const ValueSection = ({
<Input
placeholder={t('chatVariable.modal.valuePlaceholder', { ns: 'workflow' }) || ''}
value={value as number | undefined}
onChange={e => onArrayChange([Number(e.target.value)])}
onChange={(e) => {
const rawValue = e.target.value
onArrayChange([rawValue === '' ? undefined : Number(rawValue)])
}}
type="number"
/>
)}