mirror of https://github.com/langgenius/dify.git
chore: add tests
This commit is contained in:
parent
3126467ea4
commit
fd341a97f7
|
|
@ -0,0 +1,367 @@
|
|||
import type { ReactNode } from 'react'
|
||||
import type { IConfigVarProps } from './index'
|
||||
import type { ExternalDataTool } from '@/models/common'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { vi } from 'vitest'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import DebugConfigurationContext from '@/context/debug-configuration'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
import ConfigVar, { ADD_EXTERNAL_DATA_TOOL } from './index'
|
||||
|
||||
const notifySpy = vi.spyOn(Toast, 'notify').mockImplementation(vi.fn())
|
||||
|
||||
const setShowExternalDataToolModal = vi.fn()
|
||||
|
||||
type SubscriptionEvent = {
|
||||
type: string
|
||||
payload: ExternalDataTool
|
||||
}
|
||||
|
||||
let subscriptionCallback: ((event: SubscriptionEvent) => void) | null = null
|
||||
|
||||
vi.mock('@/context/event-emitter', () => ({
|
||||
useEventEmitterContextContext: () => ({
|
||||
eventEmitter: {
|
||||
useSubscription: (callback: (event: SubscriptionEvent) => void) => {
|
||||
subscriptionCallback = callback
|
||||
},
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: () => ({
|
||||
setShowExternalDataToolModal,
|
||||
}),
|
||||
}))
|
||||
|
||||
type SortableItem = {
|
||||
id: string
|
||||
variable: PromptVariable
|
||||
}
|
||||
|
||||
type SortableProps = {
|
||||
list: SortableItem[]
|
||||
setList: (list: SortableItem[]) => void
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
let latestSortableProps: SortableProps | null = null
|
||||
|
||||
vi.mock('react-sortablejs', () => ({
|
||||
ReactSortable: (props: SortableProps) => {
|
||||
latestSortableProps = props
|
||||
return <div data-testid="sortable">{props.children}</div>
|
||||
},
|
||||
}))
|
||||
|
||||
type DebugConfigurationState = any
|
||||
|
||||
const defaultDebugConfigValue: DebugConfigurationState = {
|
||||
mode: AppModeEnum.CHAT,
|
||||
dataSets: [],
|
||||
}
|
||||
|
||||
const createDebugConfigValue = (overrides: Partial<DebugConfigurationState> = {}): DebugConfigurationState => ({
|
||||
...defaultDebugConfigValue,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
let variableIndex = 0
|
||||
const createPromptVariable = (overrides: Partial<PromptVariable> = {}): PromptVariable => {
|
||||
variableIndex += 1
|
||||
return {
|
||||
key: `var_${variableIndex}`,
|
||||
name: `Variable ${variableIndex}`,
|
||||
type: 'string',
|
||||
required: false,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
const renderConfigVar = (props: Partial<IConfigVarProps> = {}, debugOverrides: Partial<DebugConfigurationState> = {}) => {
|
||||
const defaultProps: IConfigVarProps = {
|
||||
promptVariables: [],
|
||||
readonly: false,
|
||||
onPromptVariablesChange: vi.fn(),
|
||||
}
|
||||
|
||||
const mergedProps = {
|
||||
...defaultProps,
|
||||
...props,
|
||||
}
|
||||
|
||||
return render(
|
||||
<DebugConfigurationContext.Provider value={createDebugConfigValue(debugOverrides)}>
|
||||
<ConfigVar {...mergedProps} />
|
||||
</DebugConfigurationContext.Provider>,
|
||||
)
|
||||
}
|
||||
|
||||
// Rendering behavior for empty and populated states.
|
||||
describe('ConfigVar Rendering', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
latestSortableProps = null
|
||||
subscriptionCallback = null
|
||||
variableIndex = 0
|
||||
notifySpy.mockClear()
|
||||
})
|
||||
|
||||
it('should show empty state when no variables exist', () => {
|
||||
renderConfigVar({ promptVariables: [] })
|
||||
|
||||
expect(screen.getByText('appDebug.notSetVar')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render variable items and allow reordering via sortable list', () => {
|
||||
const onPromptVariablesChange = vi.fn()
|
||||
const firstVar = createPromptVariable({ key: 'first', name: 'First' })
|
||||
const secondVar = createPromptVariable({ key: 'second', name: 'Second' })
|
||||
|
||||
renderConfigVar({
|
||||
promptVariables: [firstVar, secondVar],
|
||||
onPromptVariablesChange,
|
||||
})
|
||||
|
||||
expect(screen.getByText('first')).toBeInTheDocument()
|
||||
expect(screen.getByText('second')).toBeInTheDocument()
|
||||
|
||||
act(() => {
|
||||
latestSortableProps?.setList([
|
||||
{ id: 'second', variable: secondVar },
|
||||
{ id: 'first', variable: firstVar },
|
||||
])
|
||||
})
|
||||
|
||||
expect(onPromptVariablesChange).toHaveBeenCalledWith([secondVar, firstVar])
|
||||
})
|
||||
})
|
||||
|
||||
// Variable creation flows using the add menu.
|
||||
describe('ConfigVar Add Variable', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
latestSortableProps = null
|
||||
subscriptionCallback = null
|
||||
variableIndex = 0
|
||||
notifySpy.mockClear()
|
||||
})
|
||||
|
||||
it('should add a text variable when selecting the string option', async () => {
|
||||
const onPromptVariablesChange = vi.fn()
|
||||
renderConfigVar({ promptVariables: [], onPromptVariablesChange })
|
||||
|
||||
fireEvent.click(screen.getByText('common.operation.add'))
|
||||
fireEvent.click(await screen.findByText('appDebug.variableConfig.string'))
|
||||
|
||||
expect(onPromptVariablesChange).toHaveBeenCalledTimes(1)
|
||||
const [nextVariables] = onPromptVariablesChange.mock.calls[0]
|
||||
expect(nextVariables).toHaveLength(1)
|
||||
expect(nextVariables[0].type).toBe('string')
|
||||
})
|
||||
|
||||
it('should open the external data tool modal when adding an api variable', async () => {
|
||||
const onPromptVariablesChange = vi.fn()
|
||||
renderConfigVar({ promptVariables: [], onPromptVariablesChange })
|
||||
|
||||
fireEvent.click(screen.getByText('common.operation.add'))
|
||||
fireEvent.click(await screen.findByText('appDebug.variableConfig.apiBasedVar'))
|
||||
|
||||
expect(onPromptVariablesChange).toHaveBeenCalledTimes(1)
|
||||
expect(setShowExternalDataToolModal).toHaveBeenCalledTimes(1)
|
||||
|
||||
const modalState = setShowExternalDataToolModal.mock.calls[0][0]
|
||||
expect(modalState.payload.type).toBe('api')
|
||||
|
||||
act(() => {
|
||||
modalState.onCancelCallback?.()
|
||||
})
|
||||
|
||||
expect(onPromptVariablesChange).toHaveBeenLastCalledWith([])
|
||||
})
|
||||
})
|
||||
|
||||
// Editing flows for variables through the modal.
|
||||
describe('ConfigVar Edit Variable', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
latestSortableProps = null
|
||||
subscriptionCallback = null
|
||||
variableIndex = 0
|
||||
notifySpy.mockClear()
|
||||
})
|
||||
|
||||
it('should save updates when editing a basic variable', async () => {
|
||||
const onPromptVariablesChange = vi.fn()
|
||||
const variable = createPromptVariable({ key: 'name', name: 'Name' })
|
||||
|
||||
renderConfigVar({
|
||||
promptVariables: [variable],
|
||||
onPromptVariablesChange,
|
||||
})
|
||||
|
||||
const item = screen.getByTitle('name · Name')
|
||||
const actionButtons = item.querySelectorAll('div.h-6.w-6')
|
||||
expect(actionButtons).toHaveLength(2)
|
||||
fireEvent.click(actionButtons[0])
|
||||
|
||||
const saveButton = await screen.findByRole('button', { name: 'common.operation.save' })
|
||||
fireEvent.click(saveButton)
|
||||
|
||||
expect(onPromptVariablesChange).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should show error when variable key is duplicated', async () => {
|
||||
const onPromptVariablesChange = vi.fn()
|
||||
const firstVar = createPromptVariable({ key: 'first', name: 'First' })
|
||||
const secondVar = createPromptVariable({ key: 'second', name: 'Second' })
|
||||
|
||||
renderConfigVar({
|
||||
promptVariables: [firstVar, secondVar],
|
||||
onPromptVariablesChange,
|
||||
})
|
||||
|
||||
const item = screen.getByTitle('first · First')
|
||||
const actionButtons = item.querySelectorAll('div.h-6.w-6')
|
||||
expect(actionButtons).toHaveLength(2)
|
||||
fireEvent.click(actionButtons[0])
|
||||
|
||||
const inputs = await screen.findAllByPlaceholderText('appDebug.variableConfig.inputPlaceholder')
|
||||
fireEvent.change(inputs[0], { target: { value: 'second' } })
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
expect(Toast.notify).toHaveBeenCalled()
|
||||
expect(onPromptVariablesChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show error when variable label is duplicated', async () => {
|
||||
const onPromptVariablesChange = vi.fn()
|
||||
const firstVar = createPromptVariable({ key: 'first', name: 'First' })
|
||||
const secondVar = createPromptVariable({ key: 'second', name: 'Second' })
|
||||
|
||||
renderConfigVar({
|
||||
promptVariables: [firstVar, secondVar],
|
||||
onPromptVariablesChange,
|
||||
})
|
||||
|
||||
const item = screen.getByTitle('first · First')
|
||||
const actionButtons = item.querySelectorAll('div.h-6.w-6')
|
||||
expect(actionButtons).toHaveLength(2)
|
||||
fireEvent.click(actionButtons[0])
|
||||
|
||||
const inputs = await screen.findAllByPlaceholderText('appDebug.variableConfig.inputPlaceholder')
|
||||
fireEvent.change(inputs[1], { target: { value: 'Second' } })
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
expect(Toast.notify).toHaveBeenCalled()
|
||||
expect(onPromptVariablesChange).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// Removal behavior including confirm modal branch.
|
||||
describe('ConfigVar Remove Variable', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
latestSortableProps = null
|
||||
subscriptionCallback = null
|
||||
variableIndex = 0
|
||||
notifySpy.mockClear()
|
||||
})
|
||||
|
||||
it('should remove variable directly when context confirmation is not required', () => {
|
||||
const onPromptVariablesChange = vi.fn()
|
||||
const variable = createPromptVariable({ key: 'name', name: 'Name' })
|
||||
|
||||
renderConfigVar({
|
||||
promptVariables: [variable],
|
||||
onPromptVariablesChange,
|
||||
})
|
||||
|
||||
const item = screen.getByTitle('name · Name')
|
||||
const actionButtons = item.querySelectorAll('div.h-6.w-6')
|
||||
expect(actionButtons).toHaveLength(2)
|
||||
fireEvent.click(actionButtons[1])
|
||||
|
||||
expect(onPromptVariablesChange).toHaveBeenCalledWith([])
|
||||
})
|
||||
|
||||
it('should require confirmation when removing context variable with datasets in completion mode', () => {
|
||||
const onPromptVariablesChange = vi.fn()
|
||||
const variable = createPromptVariable({
|
||||
key: 'context',
|
||||
name: 'Context',
|
||||
is_context_var: true,
|
||||
})
|
||||
|
||||
renderConfigVar(
|
||||
{
|
||||
promptVariables: [variable],
|
||||
onPromptVariablesChange,
|
||||
},
|
||||
{
|
||||
mode: AppModeEnum.COMPLETION,
|
||||
dataSets: [{ id: 'dataset-1' } as DebugConfigurationState['dataSets'][number]],
|
||||
},
|
||||
)
|
||||
|
||||
const item = screen.getByTitle('context · Context')
|
||||
const actionButtons = item.querySelectorAll('div.h-6.w-6')
|
||||
expect(actionButtons).toHaveLength(2)
|
||||
fireEvent.click(actionButtons[1])
|
||||
|
||||
expect(screen.getByText('appDebug.feature.dataSet.queryVariable.deleteContextVarTitle')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
|
||||
|
||||
expect(onPromptVariablesChange).toHaveBeenCalledWith([])
|
||||
})
|
||||
})
|
||||
|
||||
// Event subscription support for external data tools.
|
||||
describe('ConfigVar External Data Tool Events', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
latestSortableProps = null
|
||||
subscriptionCallback = null
|
||||
variableIndex = 0
|
||||
notifySpy.mockClear()
|
||||
})
|
||||
|
||||
it('should append external data tool variables from event emitter', () => {
|
||||
const onPromptVariablesChange = vi.fn()
|
||||
renderConfigVar({
|
||||
promptVariables: [],
|
||||
onPromptVariablesChange,
|
||||
})
|
||||
|
||||
act(() => {
|
||||
subscriptionCallback?.({
|
||||
type: ADD_EXTERNAL_DATA_TOOL,
|
||||
payload: {
|
||||
variable: 'api_var',
|
||||
label: 'API Var',
|
||||
enabled: true,
|
||||
type: 'api',
|
||||
config: {},
|
||||
icon: 'icon',
|
||||
icon_background: 'bg',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
expect(onPromptVariablesChange).toHaveBeenCalledWith([
|
||||
expect.objectContaining({
|
||||
key: 'api_var',
|
||||
name: 'API Var',
|
||||
required: true,
|
||||
type: 'api',
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue