mirror of
https://github.com/langgenius/dify.git
synced 2026-05-03 00:48:04 +08:00
refactor: migrate high-risk overlay follow-up selectors (#33795)
Signed-off-by: yyh <yuanyouhuilyz@gmail.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// Import component after mocks
|
||||
@ -17,44 +18,73 @@ vi.mock('@/i18n-config/language', () => ({
|
||||
],
|
||||
}))
|
||||
|
||||
// Mock PortalSelect component
|
||||
vi.mock('@/app/components/base/select', () => ({
|
||||
PortalSelect: ({
|
||||
const MockSelectContext = React.createContext<{
|
||||
value: string
|
||||
onValueChange: (value: string) => void
|
||||
}>({
|
||||
value: '',
|
||||
onValueChange: () => {},
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/base/ui/select', () => ({
|
||||
Select: ({
|
||||
value,
|
||||
items,
|
||||
onSelect,
|
||||
triggerClassName,
|
||||
popupClassName,
|
||||
popupInnerClassName,
|
||||
onValueChange,
|
||||
children,
|
||||
}: {
|
||||
value: string
|
||||
items: Array<{ value: string, name: string }>
|
||||
onSelect: (item: { value: string }) => void
|
||||
triggerClassName?: string
|
||||
popupClassName?: string
|
||||
popupInnerClassName?: string
|
||||
onValueChange: (value: string) => void
|
||||
children: React.ReactNode
|
||||
}) => (
|
||||
<div
|
||||
data-testid="portal-select"
|
||||
data-value={value}
|
||||
data-trigger-class={triggerClassName}
|
||||
data-popup-class={popupClassName}
|
||||
data-popup-inner-class={popupInnerClassName}
|
||||
>
|
||||
<span data-testid="selected-value">{value}</span>
|
||||
<div data-testid="items-container">
|
||||
{items.map(item => (
|
||||
<button
|
||||
key={item.value}
|
||||
data-testid={`select-item-${item.value}`}
|
||||
onClick={() => onSelect({ value: item.value })}
|
||||
>
|
||||
{item.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<MockSelectContext.Provider value={{ value, onValueChange }}>
|
||||
<div data-testid="select-root">{children}</div>
|
||||
</MockSelectContext.Provider>
|
||||
),
|
||||
SelectTrigger: ({
|
||||
children,
|
||||
className,
|
||||
'data-testid': testId,
|
||||
}: {
|
||||
'children': React.ReactNode
|
||||
'className'?: string
|
||||
'data-testid'?: string
|
||||
}) => (
|
||||
<button data-testid={testId ?? 'select-trigger'} data-class={className}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
SelectValue: () => {
|
||||
const { value } = React.useContext(MockSelectContext)
|
||||
return <span data-testid="selected-value">{value}</span>
|
||||
},
|
||||
SelectContent: ({
|
||||
children,
|
||||
popupClassName,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
popupClassName?: string
|
||||
}) => (
|
||||
<div data-testid="select-content" data-popup-class={popupClassName}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SelectItem: ({
|
||||
children,
|
||||
value,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
value: string
|
||||
}) => {
|
||||
const { onValueChange } = React.useContext(MockSelectContext)
|
||||
return (
|
||||
<button
|
||||
data-testid={`select-item-${value}`}
|
||||
onClick={() => onValueChange(value)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
// ==================== Test Utilities ====================
|
||||
@ -139,7 +169,7 @@ describe('TTSParamsPanel', () => {
|
||||
expect(screen.getByText('appDebug.voice.voiceSettings.voice')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render two PortalSelect components', () => {
|
||||
it('should render two Select components', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
@ -147,7 +177,7 @@ describe('TTSParamsPanel', () => {
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
const selects = screen.getAllByTestId('select-root')
|
||||
expect(selects).toHaveLength(2)
|
||||
})
|
||||
|
||||
@ -159,8 +189,8 @@ describe('TTSParamsPanel', () => {
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
expect(selects[0]).toHaveAttribute('data-value', 'zh-Hans')
|
||||
const values = screen.getAllByTestId('selected-value')
|
||||
expect(values[0]).toHaveTextContent('zh-Hans')
|
||||
})
|
||||
|
||||
it('should render voice select with correct value', () => {
|
||||
@ -171,8 +201,8 @@ describe('TTSParamsPanel', () => {
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
expect(selects[1]).toHaveAttribute('data-value', 'echo')
|
||||
const values = screen.getAllByTestId('selected-value')
|
||||
expect(values[1]).toHaveTextContent('echo')
|
||||
})
|
||||
|
||||
it('should only show supported languages in language select', () => {
|
||||
@ -205,7 +235,7 @@ describe('TTSParamsPanel', () => {
|
||||
|
||||
// ==================== Props Testing ====================
|
||||
describe('Props', () => {
|
||||
it('should apply trigger className to PortalSelect', () => {
|
||||
it('should apply trigger className to SelectTrigger', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
@ -213,12 +243,11 @@ describe('TTSParamsPanel', () => {
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
expect(selects[0]).toHaveAttribute('data-trigger-class', 'h-8')
|
||||
expect(selects[1]).toHaveAttribute('data-trigger-class', 'h-8')
|
||||
expect(screen.getByTestId('tts-language-select-trigger')).toHaveAttribute('data-class', 'w-full')
|
||||
expect(screen.getByTestId('tts-voice-select-trigger')).toHaveAttribute('data-class', 'w-full')
|
||||
})
|
||||
|
||||
it('should apply popup className to PortalSelect', () => {
|
||||
it('should apply popup className to SelectContent', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
@ -226,22 +255,9 @@ describe('TTSParamsPanel', () => {
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
expect(selects[0]).toHaveAttribute('data-popup-class', 'z-[1000]')
|
||||
expect(selects[1]).toHaveAttribute('data-popup-class', 'z-[1000]')
|
||||
})
|
||||
|
||||
it('should apply popup inner className to PortalSelect', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
expect(selects[0]).toHaveAttribute('data-popup-inner-class', 'w-[354px]')
|
||||
expect(selects[1]).toHaveAttribute('data-popup-inner-class', 'w-[354px]')
|
||||
const contents = screen.getAllByTestId('select-content')
|
||||
expect(contents[0]).toHaveAttribute('data-popup-class', 'w-[354px]')
|
||||
expect(contents[1]).toHaveAttribute('data-popup-class', 'w-[354px]')
|
||||
})
|
||||
})
|
||||
|
||||
@ -411,10 +427,8 @@ describe('TTSParamsPanel', () => {
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert - no voice items (except language items)
|
||||
const voiceSelects = screen.getAllByTestId('portal-select')
|
||||
// Second select is voice select, should have no voice items in items-container
|
||||
const voiceItemsContainer = voiceSelects[1].querySelector('[data-testid="items-container"]')
|
||||
expect(voiceItemsContainer?.children).toHaveLength(0)
|
||||
expect(screen.getAllByTestId('select-content')[1].children).toHaveLength(0)
|
||||
expect(screen.queryByTestId('select-item-alloy')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle currentModel with single voice', () => {
|
||||
@ -443,8 +457,8 @@ describe('TTSParamsPanel', () => {
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
expect(selects[0]).toHaveAttribute('data-value', '')
|
||||
const values = screen.getAllByTestId('selected-value')
|
||||
expect(values[0]).toHaveTextContent('')
|
||||
})
|
||||
|
||||
it('should handle empty voice value', () => {
|
||||
@ -455,8 +469,8 @@ describe('TTSParamsPanel', () => {
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
expect(selects[1]).toHaveAttribute('data-value', '')
|
||||
const values = screen.getAllByTestId('selected-value')
|
||||
expect(values[1]).toHaveTextContent('')
|
||||
})
|
||||
|
||||
it('should handle many voices', () => {
|
||||
@ -514,14 +528,14 @@ describe('TTSParamsPanel', () => {
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<TTSParamsPanel {...props} />)
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
expect(selects[0]).toHaveAttribute('data-value', 'en-US')
|
||||
const values = screen.getAllByTestId('selected-value')
|
||||
expect(values[0]).toHaveTextContent('en-US')
|
||||
|
||||
rerender(<TTSParamsPanel {...props} language="zh-Hans" />)
|
||||
|
||||
// Assert
|
||||
const updatedSelects = screen.getAllByTestId('portal-select')
|
||||
expect(updatedSelects[0]).toHaveAttribute('data-value', 'zh-Hans')
|
||||
const updatedValues = screen.getAllByTestId('selected-value')
|
||||
expect(updatedValues[0]).toHaveTextContent('zh-Hans')
|
||||
})
|
||||
|
||||
it('should update when voice prop changes', () => {
|
||||
@ -530,14 +544,14 @@ describe('TTSParamsPanel', () => {
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<TTSParamsPanel {...props} />)
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
expect(selects[1]).toHaveAttribute('data-value', 'alloy')
|
||||
const values = screen.getAllByTestId('selected-value')
|
||||
expect(values[1]).toHaveTextContent('alloy')
|
||||
|
||||
rerender(<TTSParamsPanel {...props} voice="echo" />)
|
||||
|
||||
// Assert
|
||||
const updatedSelects = screen.getAllByTestId('portal-select')
|
||||
expect(updatedSelects[1]).toHaveAttribute('data-value', 'echo')
|
||||
const updatedValues = screen.getAllByTestId('selected-value')
|
||||
expect(updatedValues[1]).toHaveTextContent('echo')
|
||||
})
|
||||
|
||||
it('should update voice list when currentModel changes', () => {
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PortalSelect } from '@/app/components/base/select'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/app/components/base/ui/select'
|
||||
import { languages } from '@/i18n-config/language'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
currentModel: any
|
||||
@ -12,6 +11,8 @@ type Props = {
|
||||
onChange: (language: string, voice: string) => void
|
||||
}
|
||||
|
||||
const supportedLanguages = languages.filter(item => item.supported)
|
||||
|
||||
const TTSParamsPanel = ({
|
||||
currentModel,
|
||||
language,
|
||||
@ -19,11 +20,11 @@ const TTSParamsPanel = ({
|
||||
onChange,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const voiceList = useMemo(() => {
|
||||
const voiceList = useMemo<Array<{ label: string, value: string }>>(() => {
|
||||
if (!currentModel)
|
||||
return []
|
||||
return currentModel.model_properties.voices.map((item: { mode: any }) => ({
|
||||
...item,
|
||||
return currentModel.model_properties.voices.map((item: { mode: string, name: string }) => ({
|
||||
label: item.name,
|
||||
value: item.mode,
|
||||
}))
|
||||
}, [currentModel])
|
||||
@ -39,27 +40,57 @@ const TTSParamsPanel = ({
|
||||
<div className="system-sm-semibold mb-1 flex items-center py-1 text-text-secondary">
|
||||
{t('voice.voiceSettings.language', { ns: 'appDebug' })}
|
||||
</div>
|
||||
<PortalSelect
|
||||
triggerClassName="h-8"
|
||||
popupClassName={cn('z-[1000]')}
|
||||
popupInnerClassName={cn('w-[354px]')}
|
||||
<Select
|
||||
value={language}
|
||||
items={languages.filter(item => item.supported)}
|
||||
onSelect={item => setLanguage(item.value as string)}
|
||||
/>
|
||||
onValueChange={(value) => {
|
||||
if (value == null)
|
||||
return
|
||||
setLanguage(value)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="w-full"
|
||||
data-testid="tts-language-select-trigger"
|
||||
aria-label={t('voice.voiceSettings.language', { ns: 'appDebug' })}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-[354px]">
|
||||
{supportedLanguages.map(item => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<div className="system-sm-semibold mb-1 flex items-center py-1 text-text-secondary">
|
||||
{t('voice.voiceSettings.voice', { ns: 'appDebug' })}
|
||||
</div>
|
||||
<PortalSelect
|
||||
triggerClassName="h-8"
|
||||
popupClassName={cn('z-[1000]')}
|
||||
popupInnerClassName={cn('w-[354px]')}
|
||||
<Select
|
||||
value={voice}
|
||||
items={voiceList}
|
||||
onSelect={item => setVoice(item.value as string)}
|
||||
/>
|
||||
onValueChange={(value) => {
|
||||
if (value == null)
|
||||
return
|
||||
setVoice(value)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="w-full"
|
||||
data-testid="tts-voice-select-trigger"
|
||||
aria-label={t('voice.voiceSettings.voice', { ns: 'appDebug' })}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-[354px]">
|
||||
{voiceList.map(item => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@ -1333,12 +1333,9 @@ describe('CommonCreateModal', () => {
|
||||
mockVerifyCredentials.mockImplementation((params, { onSuccess }) => {
|
||||
onSuccess()
|
||||
})
|
||||
const builder = createMockSubscriptionBuilder()
|
||||
|
||||
render(<CommonCreateModal {...defaultProps} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCreateBuilder).toHaveBeenCalled()
|
||||
})
|
||||
render(<CommonCreateModal {...defaultProps} builder={builder} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('modal-confirm'))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user