chore: tests for configuration (#29870)
This commit is contained in:
@@ -0,0 +1,227 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import ConfigVision from './index'
|
||||||
|
import ParamConfig from './param-config'
|
||||||
|
import ParamConfigContent from './param-config-content'
|
||||||
|
import type { FeatureStoreState } from '@/app/components/base/features/store'
|
||||||
|
import type { FileUpload } from '@/app/components/base/features/types'
|
||||||
|
import { Resolution, TransferMethod } from '@/types/app'
|
||||||
|
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||||
|
|
||||||
|
const mockUseContext = jest.fn()
|
||||||
|
jest.mock('use-context-selector', () => {
|
||||||
|
const actual = jest.requireActual('use-context-selector')
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useContext: (context: unknown) => mockUseContext(context),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockUseFeatures = jest.fn()
|
||||||
|
const mockUseFeaturesStore = jest.fn()
|
||||||
|
jest.mock('@/app/components/base/features/hooks', () => ({
|
||||||
|
useFeatures: (selector: (state: FeatureStoreState) => any) => mockUseFeatures(selector),
|
||||||
|
useFeaturesStore: () => mockUseFeaturesStore(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const defaultFile: FileUpload = {
|
||||||
|
enabled: false,
|
||||||
|
allowed_file_types: [],
|
||||||
|
allowed_file_upload_methods: [TransferMethod.local_file, TransferMethod.remote_url],
|
||||||
|
number_limits: 3,
|
||||||
|
image: {
|
||||||
|
enabled: false,
|
||||||
|
detail: Resolution.low,
|
||||||
|
number_limits: 3,
|
||||||
|
transfer_methods: [TransferMethod.local_file, TransferMethod.remote_url],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
let featureStoreState: FeatureStoreState
|
||||||
|
let setFeaturesMock: jest.Mock
|
||||||
|
|
||||||
|
const setupFeatureStore = (fileOverrides: Partial<FileUpload> = {}) => {
|
||||||
|
const mergedFile: FileUpload = {
|
||||||
|
...defaultFile,
|
||||||
|
...fileOverrides,
|
||||||
|
image: {
|
||||||
|
...defaultFile.image,
|
||||||
|
...fileOverrides.image,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
featureStoreState = {
|
||||||
|
features: {
|
||||||
|
file: mergedFile,
|
||||||
|
},
|
||||||
|
setFeatures: jest.fn(),
|
||||||
|
showFeaturesModal: false,
|
||||||
|
setShowFeaturesModal: jest.fn(),
|
||||||
|
}
|
||||||
|
setFeaturesMock = featureStoreState.setFeatures as jest.Mock
|
||||||
|
mockUseFeaturesStore.mockReturnValue({
|
||||||
|
getState: () => featureStoreState,
|
||||||
|
})
|
||||||
|
mockUseFeatures.mockImplementation(selector => selector(featureStoreState))
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLatestFileConfig = () => {
|
||||||
|
expect(setFeaturesMock).toHaveBeenCalled()
|
||||||
|
const latestFeatures = setFeaturesMock.mock.calls[setFeaturesMock.mock.calls.length - 1][0] as { file: FileUpload }
|
||||||
|
return latestFeatures.file
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
mockUseContext.mockReturnValue({
|
||||||
|
isShowVisionConfig: true,
|
||||||
|
isAllowVideoUpload: false,
|
||||||
|
})
|
||||||
|
setupFeatureStore()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ConfigVision handles toggling file upload types + visibility rules.
|
||||||
|
describe('ConfigVision', () => {
|
||||||
|
it('should not render when vision configuration is hidden', () => {
|
||||||
|
mockUseContext.mockReturnValue({
|
||||||
|
isShowVisionConfig: false,
|
||||||
|
isAllowVideoUpload: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
render(<ConfigVision />)
|
||||||
|
|
||||||
|
expect(screen.queryByText('appDebug.vision.name')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show the toggle and parameter controls when visible', () => {
|
||||||
|
render(<ConfigVision />)
|
||||||
|
|
||||||
|
expect(screen.getByText('appDebug.vision.name')).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'false')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should enable both image and video uploads when toggled on with video support', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
mockUseContext.mockReturnValue({
|
||||||
|
isShowVisionConfig: true,
|
||||||
|
isAllowVideoUpload: true,
|
||||||
|
})
|
||||||
|
setupFeatureStore({
|
||||||
|
allowed_file_types: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
render(<ConfigVision />)
|
||||||
|
await user.click(screen.getByRole('switch'))
|
||||||
|
|
||||||
|
const updatedFile = getLatestFileConfig()
|
||||||
|
expect(updatedFile.allowed_file_types).toEqual([SupportUploadFileTypes.image, SupportUploadFileTypes.video])
|
||||||
|
expect(updatedFile.image?.enabled).toBe(true)
|
||||||
|
expect(updatedFile.enabled).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should disable image and video uploads when toggled off and no other types remain', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
mockUseContext.mockReturnValue({
|
||||||
|
isShowVisionConfig: true,
|
||||||
|
isAllowVideoUpload: true,
|
||||||
|
})
|
||||||
|
setupFeatureStore({
|
||||||
|
allowed_file_types: [SupportUploadFileTypes.image, SupportUploadFileTypes.video],
|
||||||
|
enabled: true,
|
||||||
|
image: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
render(<ConfigVision />)
|
||||||
|
await user.click(screen.getByRole('switch'))
|
||||||
|
|
||||||
|
const updatedFile = getLatestFileConfig()
|
||||||
|
expect(updatedFile.allowed_file_types).toEqual([])
|
||||||
|
expect(updatedFile.enabled).toBe(false)
|
||||||
|
expect(updatedFile.image?.enabled).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should keep file uploads enabled when other file types remain after disabling vision', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
mockUseContext.mockReturnValue({
|
||||||
|
isShowVisionConfig: true,
|
||||||
|
isAllowVideoUpload: false,
|
||||||
|
})
|
||||||
|
setupFeatureStore({
|
||||||
|
allowed_file_types: [SupportUploadFileTypes.image, SupportUploadFileTypes.document],
|
||||||
|
enabled: true,
|
||||||
|
image: { enabled: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
render(<ConfigVision />)
|
||||||
|
await user.click(screen.getByRole('switch'))
|
||||||
|
|
||||||
|
const updatedFile = getLatestFileConfig()
|
||||||
|
expect(updatedFile.allowed_file_types).toEqual([SupportUploadFileTypes.document])
|
||||||
|
expect(updatedFile.enabled).toBe(true)
|
||||||
|
expect(updatedFile.image?.enabled).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ParamConfig exposes ParamConfigContent via an inline trigger.
|
||||||
|
describe('ParamConfig', () => {
|
||||||
|
it('should toggle parameter panel when clicking the settings button', async () => {
|
||||||
|
setupFeatureStore()
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
render(<ParamConfig />)
|
||||||
|
|
||||||
|
expect(screen.queryByText('appDebug.vision.visionSettings.title')).not.toBeInTheDocument()
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'appDebug.voice.settings' }))
|
||||||
|
|
||||||
|
expect(await screen.findByText('appDebug.vision.visionSettings.title')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ParamConfigContent manages resolution, upload source, and count limits.
|
||||||
|
describe('ParamConfigContent', () => {
|
||||||
|
it('should set resolution to high when the corresponding option is selected', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
setupFeatureStore({
|
||||||
|
image: { detail: Resolution.low },
|
||||||
|
})
|
||||||
|
|
||||||
|
render(<ParamConfigContent />)
|
||||||
|
|
||||||
|
await user.click(screen.getByText('appDebug.vision.visionSettings.high'))
|
||||||
|
|
||||||
|
const updatedFile = getLatestFileConfig()
|
||||||
|
expect(updatedFile.image?.detail).toBe(Resolution.high)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should switch upload method to local only', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
setupFeatureStore({
|
||||||
|
allowed_file_upload_methods: [TransferMethod.local_file, TransferMethod.remote_url],
|
||||||
|
})
|
||||||
|
|
||||||
|
render(<ParamConfigContent />)
|
||||||
|
|
||||||
|
await user.click(screen.getByText('appDebug.vision.visionSettings.localUpload'))
|
||||||
|
|
||||||
|
const updatedFile = getLatestFileConfig()
|
||||||
|
expect(updatedFile.allowed_file_upload_methods).toEqual([TransferMethod.local_file])
|
||||||
|
expect(updatedFile.image?.transfer_methods).toEqual([TransferMethod.local_file])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update upload limit value when input changes', async () => {
|
||||||
|
setupFeatureStore({
|
||||||
|
number_limits: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
render(<ParamConfigContent />)
|
||||||
|
const input = screen.getByRole('spinbutton') as HTMLInputElement
|
||||||
|
fireEvent.change(input, { target: { value: '4' } })
|
||||||
|
|
||||||
|
const updatedFile = getLatestFileConfig()
|
||||||
|
expect(updatedFile.number_limits).toBe(4)
|
||||||
|
expect(updatedFile.image?.number_limits).toBe(4)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import AgentSettingButton from './agent-setting-button'
|
||||||
|
import type { AgentConfig } from '@/models/debug'
|
||||||
|
import { AgentStrategy } from '@/types/app'
|
||||||
|
|
||||||
|
jest.mock('react-i18next', () => ({
|
||||||
|
useTranslation: () => ({
|
||||||
|
t: (key: string) => key,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
let latestAgentSettingProps: any
|
||||||
|
jest.mock('./agent/agent-setting', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: (props: any) => {
|
||||||
|
latestAgentSettingProps = props
|
||||||
|
return (
|
||||||
|
<div data-testid="agent-setting">
|
||||||
|
<button onClick={() => props.onSave({ ...props.payload, max_iteration: 9 })}>
|
||||||
|
save-agent
|
||||||
|
</button>
|
||||||
|
<button onClick={props.onCancel}>
|
||||||
|
cancel-agent
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const createAgentConfig = (overrides: Partial<AgentConfig> = {}): AgentConfig => ({
|
||||||
|
enabled: true,
|
||||||
|
strategy: AgentStrategy.react,
|
||||||
|
max_iteration: 3,
|
||||||
|
tools: [],
|
||||||
|
...overrides,
|
||||||
|
})
|
||||||
|
|
||||||
|
const setup = (overrides: Partial<React.ComponentProps<typeof AgentSettingButton>> = {}) => {
|
||||||
|
const props: React.ComponentProps<typeof AgentSettingButton> = {
|
||||||
|
isFunctionCall: false,
|
||||||
|
isChatModel: true,
|
||||||
|
onAgentSettingChange: jest.fn(),
|
||||||
|
agentConfig: createAgentConfig(),
|
||||||
|
...overrides,
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(<AgentSettingButton {...props} />)
|
||||||
|
return { props, user }
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
latestAgentSettingProps = undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('AgentSettingButton', () => {
|
||||||
|
it('should render button label from translation key', () => {
|
||||||
|
setup()
|
||||||
|
|
||||||
|
expect(screen.getByRole('button', { name: 'appDebug.agent.setting.name' })).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should open AgentSetting with the provided configuration when clicked', async () => {
|
||||||
|
const { user, props } = setup({ isFunctionCall: true, isChatModel: false })
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'appDebug.agent.setting.name' }))
|
||||||
|
|
||||||
|
expect(screen.getByTestId('agent-setting')).toBeInTheDocument()
|
||||||
|
expect(latestAgentSettingProps.isFunctionCall).toBe(true)
|
||||||
|
expect(latestAgentSettingProps.isChatModel).toBe(false)
|
||||||
|
expect(latestAgentSettingProps.payload).toEqual(props.agentConfig)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should call onAgentSettingChange and close when AgentSetting saves', async () => {
|
||||||
|
const { user, props } = setup()
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'appDebug.agent.setting.name' }))
|
||||||
|
await user.click(screen.getByText('save-agent'))
|
||||||
|
|
||||||
|
expect(props.onAgentSettingChange).toHaveBeenCalledTimes(1)
|
||||||
|
expect(props.onAgentSettingChange).toHaveBeenCalledWith({
|
||||||
|
...props.agentConfig,
|
||||||
|
max_iteration: 9,
|
||||||
|
})
|
||||||
|
expect(screen.queryByTestId('agent-setting')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should close AgentSetting without saving when cancel is triggered', async () => {
|
||||||
|
const { user, props } = setup()
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'appDebug.agent.setting.name' }))
|
||||||
|
await user.click(screen.getByText('cancel-agent'))
|
||||||
|
|
||||||
|
expect(props.onAgentSettingChange).not.toHaveBeenCalled()
|
||||||
|
expect(screen.queryByTestId('agent-setting')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import ConfigAudio from './config-audio'
|
||||||
|
import type { FeatureStoreState } from '@/app/components/base/features/store'
|
||||||
|
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||||
|
|
||||||
|
const mockUseContext = jest.fn()
|
||||||
|
jest.mock('use-context-selector', () => {
|
||||||
|
const actual = jest.requireActual('use-context-selector')
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useContext: (context: unknown) => mockUseContext(context),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
jest.mock('react-i18next', () => ({
|
||||||
|
useTranslation: () => ({
|
||||||
|
t: (key: string) => key,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const mockUseFeatures = jest.fn()
|
||||||
|
const mockUseFeaturesStore = jest.fn()
|
||||||
|
jest.mock('@/app/components/base/features/hooks', () => ({
|
||||||
|
useFeatures: (selector: (state: FeatureStoreState) => any) => mockUseFeatures(selector),
|
||||||
|
useFeaturesStore: () => mockUseFeaturesStore(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
type SetupOptions = {
|
||||||
|
isVisible?: boolean
|
||||||
|
allowedTypes?: SupportUploadFileTypes[]
|
||||||
|
}
|
||||||
|
|
||||||
|
let mockFeatureStoreState: FeatureStoreState
|
||||||
|
let mockSetFeatures: jest.Mock
|
||||||
|
const mockStore = {
|
||||||
|
getState: jest.fn<FeatureStoreState, []>(() => mockFeatureStoreState),
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupFeatureStore = (allowedTypes: SupportUploadFileTypes[] = []) => {
|
||||||
|
mockSetFeatures = jest.fn()
|
||||||
|
mockFeatureStoreState = {
|
||||||
|
features: {
|
||||||
|
file: {
|
||||||
|
allowed_file_types: allowedTypes,
|
||||||
|
enabled: allowedTypes.length > 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setFeatures: mockSetFeatures,
|
||||||
|
showFeaturesModal: false,
|
||||||
|
setShowFeaturesModal: jest.fn(),
|
||||||
|
}
|
||||||
|
mockStore.getState.mockImplementation(() => mockFeatureStoreState)
|
||||||
|
mockUseFeaturesStore.mockReturnValue(mockStore)
|
||||||
|
mockUseFeatures.mockImplementation(selector => selector(mockFeatureStoreState))
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderConfigAudio = (options: SetupOptions = {}) => {
|
||||||
|
const {
|
||||||
|
isVisible = true,
|
||||||
|
allowedTypes = [],
|
||||||
|
} = options
|
||||||
|
setupFeatureStore(allowedTypes)
|
||||||
|
mockUseContext.mockReturnValue({
|
||||||
|
isShowAudioConfig: isVisible,
|
||||||
|
})
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(<ConfigAudio />)
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
setFeatures: mockSetFeatures,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ConfigAudio', () => {
|
||||||
|
it('should not render when the audio configuration is hidden', () => {
|
||||||
|
renderConfigAudio({ isVisible: false })
|
||||||
|
|
||||||
|
expect(screen.queryByText('appDebug.feature.audioUpload.title')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display the audio toggle state based on feature store data', () => {
|
||||||
|
renderConfigAudio({ allowedTypes: [SupportUploadFileTypes.audio] })
|
||||||
|
|
||||||
|
expect(screen.getByText('appDebug.feature.audioUpload.title')).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should enable audio uploads when toggled on', async () => {
|
||||||
|
const { user, setFeatures } = renderConfigAudio()
|
||||||
|
const toggle = screen.getByRole('switch')
|
||||||
|
|
||||||
|
expect(toggle).toHaveAttribute('aria-checked', 'false')
|
||||||
|
await user.click(toggle)
|
||||||
|
|
||||||
|
expect(setFeatures).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
file: expect.objectContaining({
|
||||||
|
allowed_file_types: [SupportUploadFileTypes.audio],
|
||||||
|
enabled: true,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should disable audio uploads and turn off file feature when last type is removed', async () => {
|
||||||
|
const { user, setFeatures } = renderConfigAudio({ allowedTypes: [SupportUploadFileTypes.audio] })
|
||||||
|
const toggle = screen.getByRole('switch')
|
||||||
|
|
||||||
|
expect(toggle).toHaveAttribute('aria-checked', 'true')
|
||||||
|
await user.click(toggle)
|
||||||
|
|
||||||
|
expect(setFeatures).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
file: expect.objectContaining({
|
||||||
|
allowed_file_types: [],
|
||||||
|
enabled: false,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import ConfigDocument from './config-document'
|
||||||
|
import type { FeatureStoreState } from '@/app/components/base/features/store'
|
||||||
|
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||||
|
|
||||||
|
const mockUseContext = jest.fn()
|
||||||
|
jest.mock('use-context-selector', () => {
|
||||||
|
const actual = jest.requireActual('use-context-selector')
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useContext: (context: unknown) => mockUseContext(context),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockUseFeatures = jest.fn()
|
||||||
|
const mockUseFeaturesStore = jest.fn()
|
||||||
|
jest.mock('@/app/components/base/features/hooks', () => ({
|
||||||
|
useFeatures: (selector: (state: FeatureStoreState) => any) => mockUseFeatures(selector),
|
||||||
|
useFeaturesStore: () => mockUseFeaturesStore(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
type SetupOptions = {
|
||||||
|
isVisible?: boolean
|
||||||
|
allowedTypes?: SupportUploadFileTypes[]
|
||||||
|
}
|
||||||
|
|
||||||
|
let mockFeatureStoreState: FeatureStoreState
|
||||||
|
let mockSetFeatures: jest.Mock
|
||||||
|
const mockStore = {
|
||||||
|
getState: jest.fn<FeatureStoreState, []>(() => mockFeatureStoreState),
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupFeatureStore = (allowedTypes: SupportUploadFileTypes[] = []) => {
|
||||||
|
mockSetFeatures = jest.fn()
|
||||||
|
mockFeatureStoreState = {
|
||||||
|
features: {
|
||||||
|
file: {
|
||||||
|
allowed_file_types: allowedTypes,
|
||||||
|
enabled: allowedTypes.length > 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setFeatures: mockSetFeatures,
|
||||||
|
showFeaturesModal: false,
|
||||||
|
setShowFeaturesModal: jest.fn(),
|
||||||
|
}
|
||||||
|
mockStore.getState.mockImplementation(() => mockFeatureStoreState)
|
||||||
|
mockUseFeaturesStore.mockReturnValue(mockStore)
|
||||||
|
mockUseFeatures.mockImplementation(selector => selector(mockFeatureStoreState))
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderConfigDocument = (options: SetupOptions = {}) => {
|
||||||
|
const {
|
||||||
|
isVisible = true,
|
||||||
|
allowedTypes = [],
|
||||||
|
} = options
|
||||||
|
setupFeatureStore(allowedTypes)
|
||||||
|
mockUseContext.mockReturnValue({
|
||||||
|
isShowDocumentConfig: isVisible,
|
||||||
|
})
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(<ConfigDocument />)
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
setFeatures: mockSetFeatures,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ConfigDocument', () => {
|
||||||
|
it('should not render when the document configuration is hidden', () => {
|
||||||
|
renderConfigDocument({ isVisible: false })
|
||||||
|
|
||||||
|
expect(screen.queryByText('appDebug.feature.documentUpload.title')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show document toggle badge when configuration is visible', () => {
|
||||||
|
renderConfigDocument({ allowedTypes: [SupportUploadFileTypes.document] })
|
||||||
|
|
||||||
|
expect(screen.getByText('appDebug.feature.documentUpload.title')).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should add document type to allowed list when toggled on', async () => {
|
||||||
|
const { user, setFeatures } = renderConfigDocument({ allowedTypes: [SupportUploadFileTypes.audio] })
|
||||||
|
const toggle = screen.getByRole('switch')
|
||||||
|
|
||||||
|
expect(toggle).toHaveAttribute('aria-checked', 'false')
|
||||||
|
await user.click(toggle)
|
||||||
|
|
||||||
|
expect(setFeatures).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
file: expect.objectContaining({
|
||||||
|
allowed_file_types: [SupportUploadFileTypes.audio, SupportUploadFileTypes.document],
|
||||||
|
enabled: true,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove document type but keep file feature enabled when other types remain', async () => {
|
||||||
|
const { user, setFeatures } = renderConfigDocument({
|
||||||
|
allowedTypes: [SupportUploadFileTypes.document, SupportUploadFileTypes.audio],
|
||||||
|
})
|
||||||
|
const toggle = screen.getByRole('switch')
|
||||||
|
|
||||||
|
expect(toggle).toHaveAttribute('aria-checked', 'true')
|
||||||
|
await user.click(toggle)
|
||||||
|
|
||||||
|
expect(setFeatures).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
file: expect.objectContaining({
|
||||||
|
allowed_file_types: [SupportUploadFileTypes.audio],
|
||||||
|
enabled: true,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
254
web/app/components/app/configuration/config/index.spec.tsx
Normal file
254
web/app/components/app/configuration/config/index.spec.tsx
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import Config from './index'
|
||||||
|
import type { ModelConfig, PromptVariable } from '@/models/debug'
|
||||||
|
import * as useContextSelector from 'use-context-selector'
|
||||||
|
import type { ToolItem } from '@/types/app'
|
||||||
|
import { AgentStrategy, AppModeEnum, ModelModeType } from '@/types/app'
|
||||||
|
|
||||||
|
jest.mock('use-context-selector', () => {
|
||||||
|
const actual = jest.requireActual('use-context-selector')
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useContext: jest.fn(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockFormattingDispatcher = jest.fn()
|
||||||
|
jest.mock('../debug/hooks', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
useFormattingChangedDispatcher: () => mockFormattingDispatcher,
|
||||||
|
}))
|
||||||
|
|
||||||
|
let latestConfigPromptProps: any
|
||||||
|
jest.mock('@/app/components/app/configuration/config-prompt', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: (props: any) => {
|
||||||
|
latestConfigPromptProps = props
|
||||||
|
return <div data-testid="config-prompt" />
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
let latestConfigVarProps: any
|
||||||
|
jest.mock('@/app/components/app/configuration/config-var', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: (props: any) => {
|
||||||
|
latestConfigVarProps = props
|
||||||
|
return <div data-testid="config-var" />
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('../dataset-config', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => <div data-testid="dataset-config" />,
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('./agent/agent-tools', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => <div data-testid="agent-tools" />,
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('../config-vision', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => <div data-testid="config-vision" />,
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('./config-document', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => <div data-testid="config-document" />,
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('./config-audio', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => <div data-testid="config-audio" />,
|
||||||
|
}))
|
||||||
|
|
||||||
|
let latestHistoryPanelProps: any
|
||||||
|
jest.mock('../config-prompt/conversation-history/history-panel', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: (props: any) => {
|
||||||
|
latestHistoryPanelProps = props
|
||||||
|
return <div data-testid="history-panel" />
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
type MockContext = {
|
||||||
|
mode: AppModeEnum
|
||||||
|
isAdvancedMode: boolean
|
||||||
|
modelModeType: ModelModeType
|
||||||
|
isAgent: boolean
|
||||||
|
hasSetBlockStatus: {
|
||||||
|
context: boolean
|
||||||
|
history: boolean
|
||||||
|
query: boolean
|
||||||
|
}
|
||||||
|
showHistoryModal: jest.Mock
|
||||||
|
modelConfig: ModelConfig
|
||||||
|
setModelConfig: jest.Mock
|
||||||
|
setPrevPromptConfig: jest.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
const createPromptVariable = (overrides: Partial<PromptVariable> = {}): PromptVariable => ({
|
||||||
|
key: 'variable',
|
||||||
|
name: 'Variable',
|
||||||
|
type: 'string',
|
||||||
|
...overrides,
|
||||||
|
})
|
||||||
|
|
||||||
|
const createModelConfig = (overrides: Partial<ModelConfig> = {}): ModelConfig => ({
|
||||||
|
provider: 'openai',
|
||||||
|
model_id: 'gpt-4',
|
||||||
|
mode: ModelModeType.chat,
|
||||||
|
configs: {
|
||||||
|
prompt_template: 'Hello {{variable}}',
|
||||||
|
prompt_variables: [createPromptVariable({ key: 'existing' })],
|
||||||
|
},
|
||||||
|
chat_prompt_config: null,
|
||||||
|
completion_prompt_config: null,
|
||||||
|
opening_statement: null,
|
||||||
|
more_like_this: null,
|
||||||
|
suggested_questions: null,
|
||||||
|
suggested_questions_after_answer: null,
|
||||||
|
speech_to_text: null,
|
||||||
|
text_to_speech: null,
|
||||||
|
file_upload: null,
|
||||||
|
retriever_resource: null,
|
||||||
|
sensitive_word_avoidance: null,
|
||||||
|
annotation_reply: null,
|
||||||
|
external_data_tools: null,
|
||||||
|
system_parameters: {
|
||||||
|
audio_file_size_limit: 1,
|
||||||
|
file_size_limit: 1,
|
||||||
|
image_file_size_limit: 1,
|
||||||
|
video_file_size_limit: 1,
|
||||||
|
workflow_file_upload_limit: 1,
|
||||||
|
},
|
||||||
|
dataSets: [],
|
||||||
|
agentConfig: {
|
||||||
|
enabled: false,
|
||||||
|
strategy: AgentStrategy.react,
|
||||||
|
max_iteration: 1,
|
||||||
|
tools: [] as ToolItem[],
|
||||||
|
},
|
||||||
|
...overrides,
|
||||||
|
})
|
||||||
|
|
||||||
|
const createContextValue = (overrides: Partial<MockContext> = {}): MockContext => ({
|
||||||
|
mode: AppModeEnum.CHAT,
|
||||||
|
isAdvancedMode: false,
|
||||||
|
modelModeType: ModelModeType.chat,
|
||||||
|
isAgent: false,
|
||||||
|
hasSetBlockStatus: {
|
||||||
|
context: false,
|
||||||
|
history: true,
|
||||||
|
query: false,
|
||||||
|
},
|
||||||
|
showHistoryModal: jest.fn(),
|
||||||
|
modelConfig: createModelConfig(),
|
||||||
|
setModelConfig: jest.fn(),
|
||||||
|
setPrevPromptConfig: jest.fn(),
|
||||||
|
...overrides,
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockUseContext = useContextSelector.useContext as jest.Mock
|
||||||
|
|
||||||
|
const renderConfig = (contextOverrides: Partial<MockContext> = {}) => {
|
||||||
|
const contextValue = createContextValue(contextOverrides)
|
||||||
|
mockUseContext.mockReturnValue(contextValue)
|
||||||
|
return {
|
||||||
|
contextValue,
|
||||||
|
...render(<Config />),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
latestConfigPromptProps = undefined
|
||||||
|
latestConfigVarProps = undefined
|
||||||
|
latestHistoryPanelProps = undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
// Rendering scenarios ensure the layout toggles agent/history specific sections correctly.
|
||||||
|
describe('Config - Rendering', () => {
|
||||||
|
it('should render baseline sections without agent specific panels', () => {
|
||||||
|
renderConfig()
|
||||||
|
|
||||||
|
expect(screen.getByTestId('config-prompt')).toBeInTheDocument()
|
||||||
|
expect(screen.getByTestId('config-var')).toBeInTheDocument()
|
||||||
|
expect(screen.getByTestId('dataset-config')).toBeInTheDocument()
|
||||||
|
expect(screen.getByTestId('config-vision')).toBeInTheDocument()
|
||||||
|
expect(screen.getByTestId('config-document')).toBeInTheDocument()
|
||||||
|
expect(screen.getByTestId('config-audio')).toBeInTheDocument()
|
||||||
|
expect(screen.queryByTestId('agent-tools')).not.toBeInTheDocument()
|
||||||
|
expect(screen.queryByTestId('history-panel')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show AgentTools when app runs in agent mode', () => {
|
||||||
|
renderConfig({ isAgent: true })
|
||||||
|
|
||||||
|
expect(screen.getByTestId('agent-tools')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display HistoryPanel only when advanced chat completion values apply', () => {
|
||||||
|
const showHistoryModal = jest.fn()
|
||||||
|
renderConfig({
|
||||||
|
isAdvancedMode: true,
|
||||||
|
mode: AppModeEnum.ADVANCED_CHAT,
|
||||||
|
modelModeType: ModelModeType.completion,
|
||||||
|
hasSetBlockStatus: {
|
||||||
|
context: false,
|
||||||
|
history: false,
|
||||||
|
query: false,
|
||||||
|
},
|
||||||
|
showHistoryModal,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.getByTestId('history-panel')).toBeInTheDocument()
|
||||||
|
expect(latestHistoryPanelProps.showWarning).toBe(true)
|
||||||
|
expect(latestHistoryPanelProps.onShowEditModal).toBe(showHistoryModal)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Prompt handling scenarios validate integration between Config and prompt children.
|
||||||
|
describe('Config - Prompt Handling', () => {
|
||||||
|
it('should update prompt template and dispatch formatting event when text changes', () => {
|
||||||
|
const { contextValue } = renderConfig()
|
||||||
|
const previousVariables = contextValue.modelConfig.configs.prompt_variables
|
||||||
|
const additions = [createPromptVariable({ key: 'new', name: 'New' })]
|
||||||
|
|
||||||
|
latestConfigPromptProps.onChange('Updated template', additions)
|
||||||
|
|
||||||
|
expect(contextValue.setPrevPromptConfig).toHaveBeenCalledWith(contextValue.modelConfig.configs)
|
||||||
|
expect(contextValue.setModelConfig).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
configs: expect.objectContaining({
|
||||||
|
prompt_template: 'Updated template',
|
||||||
|
prompt_variables: [...previousVariables, ...additions],
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
expect(mockFormattingDispatcher).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should skip formatting dispatcher when template remains identical', () => {
|
||||||
|
const { contextValue } = renderConfig()
|
||||||
|
const unchangedTemplate = contextValue.modelConfig.configs.prompt_template
|
||||||
|
|
||||||
|
latestConfigPromptProps.onChange(unchangedTemplate, [createPromptVariable({ key: 'added' })])
|
||||||
|
|
||||||
|
expect(contextValue.setPrevPromptConfig).toHaveBeenCalledWith(contextValue.modelConfig.configs)
|
||||||
|
expect(mockFormattingDispatcher).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should replace prompt variables when ConfigVar reports updates', () => {
|
||||||
|
const { contextValue } = renderConfig()
|
||||||
|
const replacementVariables = [createPromptVariable({ key: 'replacement' })]
|
||||||
|
|
||||||
|
latestConfigVarProps.onPromptVariablesChange(replacementVariables)
|
||||||
|
|
||||||
|
expect(contextValue.setPrevPromptConfig).toHaveBeenCalledWith(contextValue.modelConfig.configs)
|
||||||
|
expect(contextValue.setModelConfig).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
configs: expect.objectContaining({
|
||||||
|
prompt_variables: replacementVariables,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user