test: add testcase for config prompt components (#29491)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Joel 2025-12-11 16:56:20 +08:00 committed by GitHub
parent 1a877bb4d0
commit 6419ce02c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 607 additions and 0 deletions

View File

@ -0,0 +1,49 @@
import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import ConfirmAddVar from './index'
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
jest.mock('../../base/var-highlight', () => ({
__esModule: true,
default: ({ name }: { name: string }) => <span data-testid="var-highlight">{name}</span>,
}))
describe('ConfirmAddVar', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('should render variable names', () => {
render(<ConfirmAddVar varNameArr={['foo', 'bar']} onConfirm={jest.fn()} onCancel={jest.fn()} onHide={jest.fn()} />)
const highlights = screen.getAllByTestId('var-highlight')
expect(highlights).toHaveLength(2)
expect(highlights[0]).toHaveTextContent('foo')
expect(highlights[1]).toHaveTextContent('bar')
})
it('should trigger cancel actions', () => {
const onConfirm = jest.fn()
const onCancel = jest.fn()
render(<ConfirmAddVar varNameArr={['foo']} onConfirm={onConfirm} onCancel={onCancel} onHide={jest.fn()} />)
fireEvent.click(screen.getByText('common.operation.cancel'))
expect(onCancel).toHaveBeenCalledTimes(1)
})
it('should trigger confirm actions', () => {
const onConfirm = jest.fn()
const onCancel = jest.fn()
render(<ConfirmAddVar varNameArr={['foo']} onConfirm={onConfirm} onCancel={onCancel} onHide={jest.fn()} />)
fireEvent.click(screen.getByText('common.operation.add'))
expect(onConfirm).toHaveBeenCalledTimes(1)
})
})

View File

@ -0,0 +1,56 @@
import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import EditModal from './edit-modal'
import type { ConversationHistoriesRole } from '@/models/debug'
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
jest.mock('@/app/components/base/modal', () => ({
__esModule: true,
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}))
describe('Conversation history edit modal', () => {
const data: ConversationHistoriesRole = {
user_prefix: 'user',
assistant_prefix: 'assistant',
}
beforeEach(() => {
jest.clearAllMocks()
})
it('should render provided prefixes', () => {
render(<EditModal isShow saveLoading={false} data={data} onClose={jest.fn()} onSave={jest.fn()} />)
expect(screen.getByDisplayValue('user')).toBeInTheDocument()
expect(screen.getByDisplayValue('assistant')).toBeInTheDocument()
})
it('should update prefixes and save changes', () => {
const onSave = jest.fn()
render(<EditModal isShow saveLoading={false} data={data} onClose={jest.fn()} onSave={onSave} />)
fireEvent.change(screen.getByDisplayValue('user'), { target: { value: 'member' } })
fireEvent.change(screen.getByDisplayValue('assistant'), { target: { value: 'helper' } })
fireEvent.click(screen.getByText('common.operation.save'))
expect(onSave).toHaveBeenCalledWith({
user_prefix: 'member',
assistant_prefix: 'helper',
})
})
it('should call close handler', () => {
const onClose = jest.fn()
render(<EditModal isShow saveLoading={false} data={data} onClose={onClose} onSave={jest.fn()} />)
fireEvent.click(screen.getByText('common.operation.cancel'))
expect(onClose).toHaveBeenCalledTimes(1)
})
})

View File

@ -0,0 +1,48 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import HistoryPanel from './history-panel'
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
const mockDocLink = jest.fn(() => 'doc-link')
jest.mock('@/context/i18n', () => ({
useDocLink: () => mockDocLink,
}))
jest.mock('@/app/components/app/configuration/base/operation-btn', () => ({
__esModule: true,
default: ({ onClick }: { onClick: () => void }) => (
<button type="button" data-testid="edit-button" onClick={onClick}>
edit
</button>
),
}))
jest.mock('@/app/components/app/configuration/base/feature-panel', () => ({
__esModule: true,
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}))
describe('HistoryPanel', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('should render warning content and link when showWarning is true', () => {
render(<HistoryPanel showWarning onShowEditModal={jest.fn()} />)
expect(screen.getByText('appDebug.feature.conversationHistory.tip')).toBeInTheDocument()
const link = screen.getByText('appDebug.feature.conversationHistory.learnMore')
expect(link).toHaveAttribute('href', 'doc-link')
})
it('should hide warning when showWarning is false', () => {
render(<HistoryPanel showWarning={false} onShowEditModal={jest.fn()} />)
expect(screen.queryByText('appDebug.feature.conversationHistory.tip')).toBeNull()
})
})

View File

@ -0,0 +1,351 @@
import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import Prompt, { type IPromptProps } from './index'
import ConfigContext from '@/context/debug-configuration'
import { MAX_PROMPT_MESSAGE_LENGTH } from '@/config'
import { type PromptItem, PromptRole, type PromptVariable } from '@/models/debug'
import { AppModeEnum, ModelModeType } from '@/types/app'
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
type DebugConfiguration = {
isAdvancedMode: boolean
currentAdvancedPrompt: PromptItem | PromptItem[]
setCurrentAdvancedPrompt: (prompt: PromptItem | PromptItem[], isUserChanged?: boolean) => void
modelModeType: ModelModeType
dataSets: Array<{
id: string
name?: string
}>
hasSetBlockStatus: {
context: boolean
history: boolean
query: boolean
}
}
const defaultPromptVariables: PromptVariable[] = [
{ key: 'var', name: 'Variable', type: 'string', required: true },
]
let mockSimplePromptInputProps: IPromptProps | null = null
jest.mock('./simple-prompt-input', () => ({
__esModule: true,
default: (props: IPromptProps) => {
mockSimplePromptInputProps = props
return (
<div
data-testid="simple-prompt-input"
data-mode={props.mode}
data-template={props.promptTemplate}
data-readonly={props.readonly ?? false}
data-no-title={props.noTitle ?? false}
data-gradient-border={props.gradientBorder ?? false}
data-editor-height={props.editorHeight ?? ''}
data-no-resize={props.noResize ?? false}
onClick={() => props.onChange?.('mocked prompt', props.promptVariables)}
>
SimplePromptInput Mock
</div>
)
},
}))
type AdvancedMessageInputProps = {
isChatMode: boolean
type: PromptRole
value: string
onTypeChange: (value: PromptRole) => void
canDelete: boolean
onDelete: () => void
onChange: (value: string) => void
promptVariables: PromptVariable[]
isContextMissing: boolean
onHideContextMissingTip: () => void
noResize?: boolean
}
jest.mock('./advanced-prompt-input', () => ({
__esModule: true,
default: (props: AdvancedMessageInputProps) => {
return (
<div
data-testid="advanced-message-input"
data-type={props.type}
data-value={props.value}
data-chat-mode={props.isChatMode}
data-can-delete={props.canDelete}
data-context-missing={props.isContextMissing}
>
<button type="button" onClick={() => props.onChange('updated text')}>
change
</button>
<button type="button" onClick={() => props.onTypeChange(PromptRole.assistant)}>
type
</button>
<button type="button" onClick={props.onDelete}>
delete
</button>
<button type="button" onClick={props.onHideContextMissingTip}>
hide-context
</button>
</div>
)
},
}))
const getContextValue = (overrides: Partial<DebugConfiguration> = {}): DebugConfiguration => {
return {
setCurrentAdvancedPrompt: jest.fn(),
isAdvancedMode: false,
currentAdvancedPrompt: [],
modelModeType: ModelModeType.chat,
dataSets: [],
hasSetBlockStatus: {
context: false,
history: false,
query: false,
},
...overrides,
}
}
const renderComponent = (
props: Partial<IPromptProps> = {},
contextOverrides: Partial<DebugConfiguration> = {},
) => {
const mergedProps: IPromptProps = {
mode: AppModeEnum.CHAT,
promptTemplate: 'initial template',
promptVariables: defaultPromptVariables,
onChange: jest.fn(),
...props,
}
const contextValue = getContextValue(contextOverrides)
return {
contextValue,
...render(
<ConfigContext.Provider value={contextValue as any}>
<Prompt {...mergedProps} />
</ConfigContext.Provider>,
),
}
}
describe('Prompt config component', () => {
beforeEach(() => {
jest.clearAllMocks()
mockSimplePromptInputProps = null
})
// Rendering simple mode
it('should render simple prompt when advanced mode is disabled', () => {
const onChange = jest.fn()
renderComponent({ onChange }, { isAdvancedMode: false })
const simplePrompt = screen.getByTestId('simple-prompt-input')
expect(simplePrompt).toBeInTheDocument()
expect(simplePrompt).toHaveAttribute('data-mode', AppModeEnum.CHAT)
expect(mockSimplePromptInputProps?.promptTemplate).toBe('initial template')
fireEvent.click(simplePrompt)
expect(onChange).toHaveBeenCalledWith('mocked prompt', defaultPromptVariables)
expect(screen.queryByTestId('advanced-message-input')).toBeNull()
})
// Rendering advanced chat messages
it('should render advanced chat prompts and show context missing tip when dataset context is not set', () => {
const currentAdvancedPrompt: PromptItem[] = [
{ role: PromptRole.user, text: 'first' },
{ role: PromptRole.assistant, text: 'second' },
]
renderComponent(
{},
{
isAdvancedMode: true,
currentAdvancedPrompt,
modelModeType: ModelModeType.chat,
dataSets: [{ id: 'ds' } as unknown as DebugConfiguration['dataSets'][number]],
hasSetBlockStatus: { context: false, history: true, query: true },
},
)
const renderedMessages = screen.getAllByTestId('advanced-message-input')
expect(renderedMessages).toHaveLength(2)
expect(renderedMessages[0]).toHaveAttribute('data-context-missing', 'true')
fireEvent.click(screen.getAllByText('hide-context')[0])
expect(screen.getAllByTestId('advanced-message-input')[0]).toHaveAttribute('data-context-missing', 'false')
})
// Chat message mutations
it('should update chat prompt value and call setter with user change flag', () => {
const currentAdvancedPrompt: PromptItem[] = [
{ role: PromptRole.user, text: 'first' },
{ role: PromptRole.assistant, text: 'second' },
]
const setCurrentAdvancedPrompt = jest.fn()
renderComponent(
{},
{
isAdvancedMode: true,
currentAdvancedPrompt,
modelModeType: ModelModeType.chat,
setCurrentAdvancedPrompt,
},
)
fireEvent.click(screen.getAllByText('change')[0])
expect(setCurrentAdvancedPrompt).toHaveBeenCalledWith(
[
{ role: PromptRole.user, text: 'updated text' },
{ role: PromptRole.assistant, text: 'second' },
],
true,
)
})
it('should update chat prompt role when type changes', () => {
const currentAdvancedPrompt: PromptItem[] = [
{ role: PromptRole.user, text: 'first' },
{ role: PromptRole.user, text: 'second' },
]
const setCurrentAdvancedPrompt = jest.fn()
renderComponent(
{},
{
isAdvancedMode: true,
currentAdvancedPrompt,
modelModeType: ModelModeType.chat,
setCurrentAdvancedPrompt,
},
)
fireEvent.click(screen.getAllByText('type')[1])
expect(setCurrentAdvancedPrompt).toHaveBeenCalledWith(
[
{ role: PromptRole.user, text: 'first' },
{ role: PromptRole.assistant, text: 'second' },
],
)
})
it('should delete chat prompt item', () => {
const currentAdvancedPrompt: PromptItem[] = [
{ role: PromptRole.user, text: 'first' },
{ role: PromptRole.assistant, text: 'second' },
]
const setCurrentAdvancedPrompt = jest.fn()
renderComponent(
{},
{
isAdvancedMode: true,
currentAdvancedPrompt,
modelModeType: ModelModeType.chat,
setCurrentAdvancedPrompt,
},
)
fireEvent.click(screen.getAllByText('delete')[0])
expect(setCurrentAdvancedPrompt).toHaveBeenCalledWith([{ role: PromptRole.assistant, text: 'second' }])
})
// Add message behavior
it('should append a mirrored role message when clicking add in chat mode', () => {
const currentAdvancedPrompt: PromptItem[] = [
{ role: PromptRole.user, text: 'first' },
]
const setCurrentAdvancedPrompt = jest.fn()
renderComponent(
{},
{
isAdvancedMode: true,
currentAdvancedPrompt,
modelModeType: ModelModeType.chat,
setCurrentAdvancedPrompt,
},
)
fireEvent.click(screen.getByText('appDebug.promptMode.operation.addMessage'))
expect(setCurrentAdvancedPrompt).toHaveBeenCalledWith([
{ role: PromptRole.user, text: 'first' },
{ role: PromptRole.assistant, text: '' },
])
})
it('should append a user role when the last chat prompt is from assistant', () => {
const currentAdvancedPrompt: PromptItem[] = [
{ role: PromptRole.assistant, text: 'reply' },
]
const setCurrentAdvancedPrompt = jest.fn()
renderComponent(
{},
{
isAdvancedMode: true,
currentAdvancedPrompt,
modelModeType: ModelModeType.chat,
setCurrentAdvancedPrompt,
},
)
fireEvent.click(screen.getByText('appDebug.promptMode.operation.addMessage'))
expect(setCurrentAdvancedPrompt).toHaveBeenCalledWith([
{ role: PromptRole.assistant, text: 'reply' },
{ role: PromptRole.user, text: '' },
])
})
it('should insert a system message when adding to an empty chat prompt list', () => {
const setCurrentAdvancedPrompt = jest.fn()
renderComponent(
{},
{
isAdvancedMode: true,
currentAdvancedPrompt: [],
modelModeType: ModelModeType.chat,
setCurrentAdvancedPrompt,
},
)
fireEvent.click(screen.getByText('appDebug.promptMode.operation.addMessage'))
expect(setCurrentAdvancedPrompt).toHaveBeenCalledWith([{ role: PromptRole.system, text: '' }])
})
it('should not show add button when reaching max prompt length', () => {
const prompts: PromptItem[] = Array.from({ length: MAX_PROMPT_MESSAGE_LENGTH }, (_, index) => ({
role: PromptRole.user,
text: `item-${index}`,
}))
renderComponent(
{},
{
isAdvancedMode: true,
currentAdvancedPrompt: prompts,
modelModeType: ModelModeType.chat,
},
)
expect(screen.queryByText('appDebug.promptMode.operation.addMessage')).toBeNull()
})
// Completion mode
it('should update completion prompt value and flag as user change', () => {
const setCurrentAdvancedPrompt = jest.fn()
renderComponent(
{},
{
isAdvancedMode: true,
currentAdvancedPrompt: { role: PromptRole.user, text: 'single' },
modelModeType: ModelModeType.completion,
setCurrentAdvancedPrompt,
},
)
fireEvent.click(screen.getByText('change'))
expect(setCurrentAdvancedPrompt).toHaveBeenCalledWith({ role: PromptRole.user, text: 'updated text' }, true)
})
})

View File

@ -0,0 +1,37 @@
import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import MessageTypeSelector from './message-type-selector'
import { PromptRole } from '@/models/debug'
describe('MessageTypeSelector', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('should render current value and keep options hidden by default', () => {
render(<MessageTypeSelector value={PromptRole.user} onChange={jest.fn()} />)
expect(screen.getByText(PromptRole.user)).toBeInTheDocument()
expect(screen.queryByText(PromptRole.system)).toBeNull()
})
it('should toggle option list when clicking the selector', () => {
render(<MessageTypeSelector value={PromptRole.system} onChange={jest.fn()} />)
fireEvent.click(screen.getByText(PromptRole.system))
expect(screen.getByText(PromptRole.user)).toBeInTheDocument()
expect(screen.getByText(PromptRole.assistant)).toBeInTheDocument()
})
it('should call onChange with selected type and close the list', () => {
const onChange = jest.fn()
render(<MessageTypeSelector value={PromptRole.assistant} onChange={onChange} />)
fireEvent.click(screen.getByText(PromptRole.assistant))
fireEvent.click(screen.getByText(PromptRole.user))
expect(onChange).toHaveBeenCalledWith(PromptRole.user)
expect(screen.queryByText(PromptRole.system)).toBeNull()
})
})

View File

@ -0,0 +1,66 @@
import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import PromptEditorHeightResizeWrap from './prompt-editor-height-resize-wrap'
describe('PromptEditorHeightResizeWrap', () => {
beforeEach(() => {
jest.clearAllMocks()
jest.useFakeTimers()
})
afterEach(() => {
jest.runOnlyPendingTimers()
jest.useRealTimers()
})
it('should render children, footer, and hide resize handler when requested', () => {
const { container } = render(
<PromptEditorHeightResizeWrap
className="wrapper"
height={150}
minHeight={100}
onHeightChange={jest.fn()}
footer={<div>footer</div>}
hideResize
>
<div>content</div>
</PromptEditorHeightResizeWrap>,
)
expect(screen.getByText('content')).toBeInTheDocument()
expect(screen.getByText('footer')).toBeInTheDocument()
expect(container.querySelector('.cursor-row-resize')).toBeNull()
})
it('should resize height with mouse events and clamp to minHeight', () => {
const onHeightChange = jest.fn()
const { container } = render(
<PromptEditorHeightResizeWrap
height={150}
minHeight={100}
onHeightChange={onHeightChange}
>
<div>content</div>
</PromptEditorHeightResizeWrap>,
)
const handle = container.querySelector('.cursor-row-resize')
expect(handle).not.toBeNull()
fireEvent.mouseDown(handle as Element, { clientY: 100 })
expect(document.body.style.userSelect).toBe('none')
fireEvent.mouseMove(document, { clientY: 130 })
jest.runAllTimers()
expect(onHeightChange).toHaveBeenLastCalledWith(180)
onHeightChange.mockClear()
fireEvent.mouseMove(document, { clientY: -100 })
jest.runAllTimers()
expect(onHeightChange).toHaveBeenLastCalledWith(100)
fireEvent.mouseUp(document)
expect(document.body.style.userSelect).toBe('')
})
})