mirror of https://github.com/langgenius/dify.git
test: add unit tests for plugin detail panel components including model selector, TTS parameters, and subscription management
This commit is contained in:
parent
0b1439fee4
commit
5a90b027ac
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,717 @@
|
|||
import type { FormValue, ModelParameterRule } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// Import component after mocks
|
||||
import LLMParamsPanel from './llm-params-panel'
|
||||
|
||||
// ==================== Mock Setup ====================
|
||||
// All vi.mock() calls are hoisted, so inline all mock data
|
||||
|
||||
// Mock useModelParameterRules hook
|
||||
const mockUseModelParameterRules = vi.fn()
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
useModelParameterRules: (provider: string, modelId: string) => mockUseModelParameterRules(provider, modelId),
|
||||
}))
|
||||
|
||||
// Mock config constants with inline data
|
||||
vi.mock('@/config', () => ({
|
||||
TONE_LIST: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Creative',
|
||||
config: {
|
||||
temperature: 0.8,
|
||||
top_p: 0.9,
|
||||
presence_penalty: 0.1,
|
||||
frequency_penalty: 0.1,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Balanced',
|
||||
config: {
|
||||
temperature: 0.5,
|
||||
top_p: 0.85,
|
||||
presence_penalty: 0.2,
|
||||
frequency_penalty: 0.3,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Precise',
|
||||
config: {
|
||||
temperature: 0.2,
|
||||
top_p: 0.75,
|
||||
presence_penalty: 0.5,
|
||||
frequency_penalty: 0.5,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Custom',
|
||||
},
|
||||
],
|
||||
STOP_PARAMETER_RULE: {
|
||||
default: [],
|
||||
help: {
|
||||
en_US: 'Stop sequences help text',
|
||||
zh_Hans: '停止序列帮助文本',
|
||||
},
|
||||
label: {
|
||||
en_US: 'Stop sequences',
|
||||
zh_Hans: '停止序列',
|
||||
},
|
||||
name: 'stop',
|
||||
required: false,
|
||||
type: 'tag',
|
||||
tagPlaceholder: {
|
||||
en_US: 'Enter sequence and press Tab',
|
||||
zh_Hans: '输入序列并按 Tab 键',
|
||||
},
|
||||
},
|
||||
PROVIDER_WITH_PRESET_TONE: ['langgenius/openai/openai', 'langgenius/azure_openai/azure_openai'],
|
||||
}))
|
||||
|
||||
// Mock PresetsParameter component
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter', () => ({
|
||||
default: ({ onSelect }: { onSelect: (toneId: number) => void }) => (
|
||||
<div data-testid="presets-parameter">
|
||||
<button data-testid="preset-creative" onClick={() => onSelect(1)}>Creative</button>
|
||||
<button data-testid="preset-balanced" onClick={() => onSelect(2)}>Balanced</button>
|
||||
<button data-testid="preset-precise" onClick={() => onSelect(3)}>Precise</button>
|
||||
<button data-testid="preset-custom" onClick={() => onSelect(4)}>Custom</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock ParameterItem component
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/model-parameter-modal/parameter-item', () => ({
|
||||
default: ({ parameterRule, value, onChange, onSwitch, isInWorkflow }: {
|
||||
parameterRule: { name: string, label: { en_US: string }, default?: unknown }
|
||||
value: unknown
|
||||
onChange: (v: unknown) => void
|
||||
onSwitch: (checked: boolean, assignValue: unknown) => void
|
||||
isInWorkflow?: boolean
|
||||
}) => (
|
||||
<div
|
||||
data-testid={`parameter-item-${parameterRule.name}`}
|
||||
data-value={JSON.stringify(value)}
|
||||
data-is-in-workflow={isInWorkflow}
|
||||
>
|
||||
<span>{parameterRule.label.en_US}</span>
|
||||
<button data-testid={`change-${parameterRule.name}`} onClick={() => onChange(0.5)}>Change</button>
|
||||
<button data-testid={`switch-on-${parameterRule.name}`} onClick={() => onSwitch(true, parameterRule.default)}>Switch On</button>
|
||||
<button data-testid={`switch-off-${parameterRule.name}`} onClick={() => onSwitch(false, parameterRule.default)}>Switch Off</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// ==================== Test Utilities ====================
|
||||
|
||||
/**
|
||||
* Factory function to create a ModelParameterRule with defaults
|
||||
*/
|
||||
const createParameterRule = (overrides: Partial<ModelParameterRule> = {}): ModelParameterRule => ({
|
||||
name: 'temperature',
|
||||
label: { en_US: 'Temperature', zh_Hans: '温度' },
|
||||
type: 'float',
|
||||
default: 0.7,
|
||||
min: 0,
|
||||
max: 2,
|
||||
precision: 2,
|
||||
required: false,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
/**
|
||||
* Factory function to create default props
|
||||
*/
|
||||
const createDefaultProps = (overrides: Partial<{
|
||||
isAdvancedMode: boolean
|
||||
provider: string
|
||||
modelId: string
|
||||
completionParams: FormValue
|
||||
onCompletionParamsChange: (newParams: FormValue) => void
|
||||
}> = {}) => ({
|
||||
isAdvancedMode: false,
|
||||
provider: 'langgenius/openai/openai',
|
||||
modelId: 'gpt-4',
|
||||
completionParams: {},
|
||||
onCompletionParamsChange: vi.fn(),
|
||||
...overrides,
|
||||
})
|
||||
|
||||
/**
|
||||
* Setup mock for useModelParameterRules
|
||||
*/
|
||||
const setupModelParameterRulesMock = (config: {
|
||||
data?: ModelParameterRule[]
|
||||
isPending?: boolean
|
||||
} = {}) => {
|
||||
mockUseModelParameterRules.mockReturnValue({
|
||||
data: config.data ? { data: config.data } : undefined,
|
||||
isPending: config.isPending ?? false,
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== Tests ====================
|
||||
|
||||
describe('LLMParamsPanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setupModelParameterRulesMock({ data: [], isPending: false })
|
||||
})
|
||||
|
||||
// ==================== Rendering Tests ====================
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
const { container } = render(<LLMParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(container).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render loading state when isPending is true', () => {
|
||||
// Arrange
|
||||
setupModelParameterRulesMock({ isPending: true })
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
|
||||
// Assert - Loading component uses aria-label instead of visible text
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render parameters header', () => {
|
||||
// Arrange
|
||||
setupModelParameterRulesMock({ data: [], isPending: false })
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('common.modelProvider.parameters')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render PresetsParameter for openai provider', () => {
|
||||
// Arrange
|
||||
setupModelParameterRulesMock({ data: [], isPending: false })
|
||||
const props = createDefaultProps({ provider: 'langgenius/openai/openai' })
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('presets-parameter')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render PresetsParameter for azure_openai provider', () => {
|
||||
// Arrange
|
||||
setupModelParameterRulesMock({ data: [], isPending: false })
|
||||
const props = createDefaultProps({ provider: 'langgenius/azure_openai/azure_openai' })
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('presets-parameter')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render PresetsParameter for non-preset providers', () => {
|
||||
// Arrange
|
||||
setupModelParameterRulesMock({ data: [], isPending: false })
|
||||
const props = createDefaultProps({ provider: 'anthropic/claude' })
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByTestId('presets-parameter')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render parameter items when rules are available', () => {
|
||||
// Arrange
|
||||
const rules = [
|
||||
createParameterRule({ name: 'temperature' }),
|
||||
createParameterRule({ name: 'top_p', label: { en_US: 'Top P', zh_Hans: 'Top P' } }),
|
||||
]
|
||||
setupModelParameterRulesMock({ data: rules, isPending: false })
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('parameter-item-temperature')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('parameter-item-top_p')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render parameter items when rules are empty', () => {
|
||||
// Arrange
|
||||
setupModelParameterRulesMock({ data: [], isPending: false })
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByTestId('parameter-item-temperature')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should include stop parameter rule in advanced mode', () => {
|
||||
// Arrange
|
||||
const rules = [createParameterRule({ name: 'temperature' })]
|
||||
setupModelParameterRulesMock({ data: rules, isPending: false })
|
||||
const props = createDefaultProps({ isAdvancedMode: true })
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('parameter-item-temperature')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('parameter-item-stop')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not include stop parameter rule in non-advanced mode', () => {
|
||||
// Arrange
|
||||
const rules = [createParameterRule({ name: 'temperature' })]
|
||||
setupModelParameterRulesMock({ data: rules, isPending: false })
|
||||
const props = createDefaultProps({ isAdvancedMode: false })
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('parameter-item-temperature')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('parameter-item-stop')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass isInWorkflow=true to ParameterItem', () => {
|
||||
// Arrange
|
||||
const rules = [createParameterRule({ name: 'temperature' })]
|
||||
setupModelParameterRulesMock({ data: rules, isPending: false })
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('parameter-item-temperature')).toHaveAttribute('data-is-in-workflow', 'true')
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Props Testing ====================
|
||||
describe('Props', () => {
|
||||
it('should call useModelParameterRules with provider and modelId', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({
|
||||
provider: 'test-provider',
|
||||
modelId: 'test-model',
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(mockUseModelParameterRules).toHaveBeenCalledWith('test-provider', 'test-model')
|
||||
})
|
||||
|
||||
it('should pass completion params value to ParameterItem', () => {
|
||||
// Arrange
|
||||
const rules = [createParameterRule({ name: 'temperature' })]
|
||||
setupModelParameterRulesMock({ data: rules, isPending: false })
|
||||
const props = createDefaultProps({
|
||||
completionParams: { temperature: 0.8 },
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('parameter-item-temperature')).toHaveAttribute('data-value', '0.8')
|
||||
})
|
||||
|
||||
it('should handle undefined completion params value', () => {
|
||||
// Arrange
|
||||
const rules = [createParameterRule({ name: 'temperature' })]
|
||||
setupModelParameterRulesMock({ data: rules, isPending: false })
|
||||
const props = createDefaultProps({
|
||||
completionParams: {},
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
|
||||
// Assert - when value is undefined, JSON.stringify returns undefined string
|
||||
expect(screen.getByTestId('parameter-item-temperature')).not.toHaveAttribute('data-value')
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Event Handlers ====================
|
||||
describe('Event Handlers', () => {
|
||||
describe('handleSelectPresetParameter', () => {
|
||||
it('should apply Creative preset config', () => {
|
||||
// Arrange
|
||||
const onCompletionParamsChange = vi.fn()
|
||||
setupModelParameterRulesMock({ data: [], isPending: false })
|
||||
const props = createDefaultProps({
|
||||
provider: 'langgenius/openai/openai',
|
||||
onCompletionParamsChange,
|
||||
completionParams: { existing: 'value' },
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
fireEvent.click(screen.getByTestId('preset-creative'))
|
||||
|
||||
// Assert
|
||||
expect(onCompletionParamsChange).toHaveBeenCalledWith({
|
||||
existing: 'value',
|
||||
temperature: 0.8,
|
||||
top_p: 0.9,
|
||||
presence_penalty: 0.1,
|
||||
frequency_penalty: 0.1,
|
||||
})
|
||||
})
|
||||
|
||||
it('should apply Balanced preset config', () => {
|
||||
// Arrange
|
||||
const onCompletionParamsChange = vi.fn()
|
||||
setupModelParameterRulesMock({ data: [], isPending: false })
|
||||
const props = createDefaultProps({
|
||||
provider: 'langgenius/openai/openai',
|
||||
onCompletionParamsChange,
|
||||
completionParams: {},
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
fireEvent.click(screen.getByTestId('preset-balanced'))
|
||||
|
||||
// Assert
|
||||
expect(onCompletionParamsChange).toHaveBeenCalledWith({
|
||||
temperature: 0.5,
|
||||
top_p: 0.85,
|
||||
presence_penalty: 0.2,
|
||||
frequency_penalty: 0.3,
|
||||
})
|
||||
})
|
||||
|
||||
it('should apply Precise preset config', () => {
|
||||
// Arrange
|
||||
const onCompletionParamsChange = vi.fn()
|
||||
setupModelParameterRulesMock({ data: [], isPending: false })
|
||||
const props = createDefaultProps({
|
||||
provider: 'langgenius/openai/openai',
|
||||
onCompletionParamsChange,
|
||||
completionParams: {},
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
fireEvent.click(screen.getByTestId('preset-precise'))
|
||||
|
||||
// Assert
|
||||
expect(onCompletionParamsChange).toHaveBeenCalledWith({
|
||||
temperature: 0.2,
|
||||
top_p: 0.75,
|
||||
presence_penalty: 0.5,
|
||||
frequency_penalty: 0.5,
|
||||
})
|
||||
})
|
||||
|
||||
it('should apply empty config for Custom preset (spreads undefined)', () => {
|
||||
// Arrange
|
||||
const onCompletionParamsChange = vi.fn()
|
||||
setupModelParameterRulesMock({ data: [], isPending: false })
|
||||
const props = createDefaultProps({
|
||||
provider: 'langgenius/openai/openai',
|
||||
onCompletionParamsChange,
|
||||
completionParams: { existing: 'value' },
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
fireEvent.click(screen.getByTestId('preset-custom'))
|
||||
|
||||
// Assert - Custom preset has no config, so only existing params are kept
|
||||
expect(onCompletionParamsChange).toHaveBeenCalledWith({ existing: 'value' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleParamChange', () => {
|
||||
it('should call onCompletionParamsChange with updated param', () => {
|
||||
// Arrange
|
||||
const onCompletionParamsChange = vi.fn()
|
||||
const rules = [createParameterRule({ name: 'temperature' })]
|
||||
setupModelParameterRulesMock({ data: rules, isPending: false })
|
||||
const props = createDefaultProps({
|
||||
onCompletionParamsChange,
|
||||
completionParams: { existing: 'value' },
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
fireEvent.click(screen.getByTestId('change-temperature'))
|
||||
|
||||
// Assert
|
||||
expect(onCompletionParamsChange).toHaveBeenCalledWith({
|
||||
existing: 'value',
|
||||
temperature: 0.5,
|
||||
})
|
||||
})
|
||||
|
||||
it('should override existing param value', () => {
|
||||
// Arrange
|
||||
const onCompletionParamsChange = vi.fn()
|
||||
const rules = [createParameterRule({ name: 'temperature' })]
|
||||
setupModelParameterRulesMock({ data: rules, isPending: false })
|
||||
const props = createDefaultProps({
|
||||
onCompletionParamsChange,
|
||||
completionParams: { temperature: 0.9 },
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
fireEvent.click(screen.getByTestId('change-temperature'))
|
||||
|
||||
// Assert
|
||||
expect(onCompletionParamsChange).toHaveBeenCalledWith({
|
||||
temperature: 0.5,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleSwitch', () => {
|
||||
it('should add param when switch is turned on', () => {
|
||||
// Arrange
|
||||
const onCompletionParamsChange = vi.fn()
|
||||
const rules = [createParameterRule({ name: 'temperature', default: 0.7 })]
|
||||
setupModelParameterRulesMock({ data: rules, isPending: false })
|
||||
const props = createDefaultProps({
|
||||
onCompletionParamsChange,
|
||||
completionParams: { existing: 'value' },
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
fireEvent.click(screen.getByTestId('switch-on-temperature'))
|
||||
|
||||
// Assert
|
||||
expect(onCompletionParamsChange).toHaveBeenCalledWith({
|
||||
existing: 'value',
|
||||
temperature: 0.7,
|
||||
})
|
||||
})
|
||||
|
||||
it('should remove param when switch is turned off', () => {
|
||||
// Arrange
|
||||
const onCompletionParamsChange = vi.fn()
|
||||
const rules = [createParameterRule({ name: 'temperature' })]
|
||||
setupModelParameterRulesMock({ data: rules, isPending: false })
|
||||
const props = createDefaultProps({
|
||||
onCompletionParamsChange,
|
||||
completionParams: { temperature: 0.8, other: 'value' },
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
fireEvent.click(screen.getByTestId('switch-off-temperature'))
|
||||
|
||||
// Assert
|
||||
expect(onCompletionParamsChange).toHaveBeenCalledWith({
|
||||
other: 'value',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Memoization ====================
|
||||
describe('Memoization - parameterRules', () => {
|
||||
it('should return empty array when data is undefined', () => {
|
||||
// Arrange
|
||||
mockUseModelParameterRules.mockReturnValue({
|
||||
data: undefined,
|
||||
isPending: false,
|
||||
})
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
|
||||
// Assert - no parameter items should be rendered
|
||||
expect(screen.queryByTestId(/parameter-item-/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return empty array when data.data is undefined', () => {
|
||||
// Arrange
|
||||
mockUseModelParameterRules.mockReturnValue({
|
||||
data: { data: undefined },
|
||||
isPending: false,
|
||||
})
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByTestId(/parameter-item-/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use data.data when available', () => {
|
||||
// Arrange
|
||||
const rules = [
|
||||
createParameterRule({ name: 'temperature' }),
|
||||
createParameterRule({ name: 'top_p' }),
|
||||
]
|
||||
setupModelParameterRulesMock({ data: rules, isPending: false })
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('parameter-item-temperature')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('parameter-item-top_p')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Edge Cases ====================
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty completionParams', () => {
|
||||
// Arrange
|
||||
const rules = [createParameterRule({ name: 'temperature' })]
|
||||
setupModelParameterRulesMock({ data: rules, isPending: false })
|
||||
const props = createDefaultProps({ completionParams: {} })
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('parameter-item-temperature')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle multiple parameter rules', () => {
|
||||
// Arrange
|
||||
const rules = [
|
||||
createParameterRule({ name: 'temperature' }),
|
||||
createParameterRule({ name: 'top_p' }),
|
||||
createParameterRule({ name: 'max_tokens', type: 'int' }),
|
||||
createParameterRule({ name: 'presence_penalty' }),
|
||||
]
|
||||
setupModelParameterRulesMock({ data: rules, isPending: false })
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<LLMParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('parameter-item-temperature')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('parameter-item-top_p')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('parameter-item-max_tokens')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('parameter-item-presence_penalty')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use unique keys for parameter items based on modelId and name', () => {
|
||||
// Arrange
|
||||
const rules = [
|
||||
createParameterRule({ name: 'temperature' }),
|
||||
createParameterRule({ name: 'top_p' }),
|
||||
]
|
||||
setupModelParameterRulesMock({ data: rules, isPending: false })
|
||||
const props = createDefaultProps({ modelId: 'gpt-4' })
|
||||
|
||||
// Act
|
||||
const { container } = render(<LLMParamsPanel {...props} />)
|
||||
|
||||
// Assert - verify both items are rendered (keys are internal but rendering proves uniqueness)
|
||||
const items = container.querySelectorAll('[data-testid^="parameter-item-"]')
|
||||
expect(items).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Re-render Behavior ====================
|
||||
describe('Re-render Behavior', () => {
|
||||
it('should update parameter items when rules change', () => {
|
||||
// Arrange
|
||||
const initialRules = [createParameterRule({ name: 'temperature' })]
|
||||
setupModelParameterRulesMock({ data: initialRules, isPending: false })
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<LLMParamsPanel {...props} />)
|
||||
expect(screen.getByTestId('parameter-item-temperature')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('parameter-item-top_p')).not.toBeInTheDocument()
|
||||
|
||||
// Update mock
|
||||
const newRules = [
|
||||
createParameterRule({ name: 'temperature' }),
|
||||
createParameterRule({ name: 'top_p' }),
|
||||
]
|
||||
setupModelParameterRulesMock({ data: newRules, isPending: false })
|
||||
rerender(<LLMParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('parameter-item-temperature')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('parameter-item-top_p')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show loading when transitioning from loaded to loading', () => {
|
||||
// Arrange
|
||||
const rules = [createParameterRule({ name: 'temperature' })]
|
||||
setupModelParameterRulesMock({ data: rules, isPending: false })
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<LLMParamsPanel {...props} />)
|
||||
expect(screen.getByTestId('parameter-item-temperature')).toBeInTheDocument()
|
||||
|
||||
// Update to loading
|
||||
setupModelParameterRulesMock({ isPending: true })
|
||||
rerender(<LLMParamsPanel {...props} />)
|
||||
|
||||
// Assert - Loading component uses role="status" with aria-label
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should update when isAdvancedMode changes', () => {
|
||||
// Arrange
|
||||
const rules = [createParameterRule({ name: 'temperature' })]
|
||||
setupModelParameterRulesMock({ data: rules, isPending: false })
|
||||
const props = createDefaultProps({ isAdvancedMode: false })
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<LLMParamsPanel {...props} />)
|
||||
expect(screen.queryByTestId('parameter-item-stop')).not.toBeInTheDocument()
|
||||
|
||||
rerender(<LLMParamsPanel {...props} isAdvancedMode={true} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('parameter-item-stop')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Component Type ====================
|
||||
describe('Component Type', () => {
|
||||
it('should be a functional component', () => {
|
||||
// Assert
|
||||
expect(typeof LLMParamsPanel).toBe('function')
|
||||
})
|
||||
|
||||
it('should accept all required props', () => {
|
||||
// Arrange
|
||||
setupModelParameterRulesMock({ data: [], isPending: false })
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act & Assert
|
||||
expect(() => render(<LLMParamsPanel {...props} />)).not.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,623 @@
|
|||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// Import component after mocks
|
||||
import TTSParamsPanel from './tts-params-panel'
|
||||
|
||||
// ==================== Mock Setup ====================
|
||||
// All vi.mock() calls are hoisted, so inline all mock data
|
||||
|
||||
// Mock languages data with inline definition
|
||||
vi.mock('@/i18n-config/language', () => ({
|
||||
languages: [
|
||||
{ value: 'en-US', name: 'English (United States)', supported: true },
|
||||
{ value: 'zh-Hans', name: '简体中文', supported: true },
|
||||
{ value: 'ja-JP', name: '日本語', supported: true },
|
||||
{ value: 'unsupported-lang', name: 'Unsupported Language', supported: false },
|
||||
],
|
||||
}))
|
||||
|
||||
// Mock PortalSelect component
|
||||
vi.mock('@/app/components/base/select', () => ({
|
||||
PortalSelect: ({
|
||||
value,
|
||||
items,
|
||||
onSelect,
|
||||
triggerClassName,
|
||||
popupClassName,
|
||||
popupInnerClassName,
|
||||
}: {
|
||||
value: string
|
||||
items: Array<{ value: string, name: string }>
|
||||
onSelect: (item: { value: string }) => void
|
||||
triggerClassName?: string
|
||||
popupClassName?: string
|
||||
popupInnerClassName?: string
|
||||
}) => (
|
||||
<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>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// ==================== Test Utilities ====================
|
||||
|
||||
/**
|
||||
* Factory function to create a voice item
|
||||
*/
|
||||
const createVoiceItem = (overrides: Partial<{ mode: string, name: string }> = {}) => ({
|
||||
mode: 'alloy',
|
||||
name: 'Alloy',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
/**
|
||||
* Factory function to create a currentModel with voices
|
||||
*/
|
||||
const createCurrentModel = (voices: Array<{ mode: string, name: string }> = []) => ({
|
||||
model_properties: {
|
||||
voices,
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Factory function to create default props
|
||||
*/
|
||||
const createDefaultProps = (overrides: Partial<{
|
||||
currentModel: { model_properties: { voices: Array<{ mode: string, name: string }> } } | null
|
||||
language: string
|
||||
voice: string
|
||||
onChange: (language: string, voice: string) => void
|
||||
}> = {}) => ({
|
||||
currentModel: createCurrentModel([
|
||||
createVoiceItem({ mode: 'alloy', name: 'Alloy' }),
|
||||
createVoiceItem({ mode: 'echo', name: 'Echo' }),
|
||||
createVoiceItem({ mode: 'fable', name: 'Fable' }),
|
||||
]),
|
||||
language: 'en-US',
|
||||
voice: 'alloy',
|
||||
onChange: vi.fn(),
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// ==================== Tests ====================
|
||||
|
||||
describe('TTSParamsPanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// ==================== Rendering Tests ====================
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
const { container } = render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(container).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render language label', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('appDebug.voice.voiceSettings.language')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render voice label', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('appDebug.voice.voiceSettings.voice')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render two PortalSelect components', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
expect(selects).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should render language select with correct value', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ language: 'zh-Hans' })
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
expect(selects[0]).toHaveAttribute('data-value', 'zh-Hans')
|
||||
})
|
||||
|
||||
it('should render voice select with correct value', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ voice: 'echo' })
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
expect(selects[1]).toHaveAttribute('data-value', 'echo')
|
||||
})
|
||||
|
||||
it('should only show supported languages in language select', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('select-item-en-US')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('select-item-zh-Hans')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('select-item-ja-JP')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('select-item-unsupported-lang')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render voice items from currentModel', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('select-item-alloy')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('select-item-echo')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('select-item-fable')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Props Testing ====================
|
||||
describe('Props', () => {
|
||||
it('should apply trigger className to PortalSelect', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
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')
|
||||
})
|
||||
|
||||
it('should apply popup className to PortalSelect', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
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]')
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Event Handlers ====================
|
||||
describe('Event Handlers', () => {
|
||||
describe('setLanguage', () => {
|
||||
it('should call onChange with new language and current voice', () => {
|
||||
// Arrange
|
||||
const onChange = vi.fn()
|
||||
const props = createDefaultProps({
|
||||
onChange,
|
||||
language: 'en-US',
|
||||
voice: 'alloy',
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
fireEvent.click(screen.getByTestId('select-item-zh-Hans'))
|
||||
|
||||
// Assert
|
||||
expect(onChange).toHaveBeenCalledWith('zh-Hans', 'alloy')
|
||||
})
|
||||
|
||||
it('should call onChange with different languages', () => {
|
||||
// Arrange
|
||||
const onChange = vi.fn()
|
||||
const props = createDefaultProps({
|
||||
onChange,
|
||||
language: 'en-US',
|
||||
voice: 'echo',
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
fireEvent.click(screen.getByTestId('select-item-ja-JP'))
|
||||
|
||||
// Assert
|
||||
expect(onChange).toHaveBeenCalledWith('ja-JP', 'echo')
|
||||
})
|
||||
|
||||
it('should preserve voice when changing language', () => {
|
||||
// Arrange
|
||||
const onChange = vi.fn()
|
||||
const props = createDefaultProps({
|
||||
onChange,
|
||||
language: 'en-US',
|
||||
voice: 'fable',
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
fireEvent.click(screen.getByTestId('select-item-zh-Hans'))
|
||||
|
||||
// Assert
|
||||
expect(onChange).toHaveBeenCalledWith('zh-Hans', 'fable')
|
||||
})
|
||||
})
|
||||
|
||||
describe('setVoice', () => {
|
||||
it('should call onChange with current language and new voice', () => {
|
||||
// Arrange
|
||||
const onChange = vi.fn()
|
||||
const props = createDefaultProps({
|
||||
onChange,
|
||||
language: 'en-US',
|
||||
voice: 'alloy',
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
fireEvent.click(screen.getByTestId('select-item-echo'))
|
||||
|
||||
// Assert
|
||||
expect(onChange).toHaveBeenCalledWith('en-US', 'echo')
|
||||
})
|
||||
|
||||
it('should call onChange with different voices', () => {
|
||||
// Arrange
|
||||
const onChange = vi.fn()
|
||||
const props = createDefaultProps({
|
||||
onChange,
|
||||
language: 'zh-Hans',
|
||||
voice: 'alloy',
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
fireEvent.click(screen.getByTestId('select-item-fable'))
|
||||
|
||||
// Assert
|
||||
expect(onChange).toHaveBeenCalledWith('zh-Hans', 'fable')
|
||||
})
|
||||
|
||||
it('should preserve language when changing voice', () => {
|
||||
// Arrange
|
||||
const onChange = vi.fn()
|
||||
const props = createDefaultProps({
|
||||
onChange,
|
||||
language: 'ja-JP',
|
||||
voice: 'alloy',
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
fireEvent.click(screen.getByTestId('select-item-echo'))
|
||||
|
||||
// Assert
|
||||
expect(onChange).toHaveBeenCalledWith('ja-JP', 'echo')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Memoization ====================
|
||||
describe('Memoization - voiceList', () => {
|
||||
it('should return empty array when currentModel is null', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ currentModel: null })
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert - no voice items should be rendered
|
||||
expect(screen.queryByTestId('select-item-alloy')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('select-item-echo')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return empty array when currentModel is undefined', () => {
|
||||
// Arrange
|
||||
const props = {
|
||||
currentModel: undefined,
|
||||
language: 'en-US',
|
||||
voice: 'alloy',
|
||||
onChange: vi.fn(),
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByTestId('select-item-alloy')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should map voices with mode as value', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({
|
||||
currentModel: createCurrentModel([
|
||||
{ mode: 'voice-1', name: 'Voice One' },
|
||||
{ mode: 'voice-2', name: 'Voice Two' },
|
||||
]),
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('select-item-voice-1')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('select-item-voice-2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle currentModel with empty voices array', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({
|
||||
currentModel: createCurrentModel([]),
|
||||
})
|
||||
|
||||
// Act
|
||||
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)
|
||||
})
|
||||
|
||||
it('should handle currentModel with single voice', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({
|
||||
currentModel: createCurrentModel([
|
||||
{ mode: 'single-voice', name: 'Single Voice' },
|
||||
]),
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('select-item-single-voice')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Edge Cases ====================
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty language value', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ language: '' })
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
expect(selects[0]).toHaveAttribute('data-value', '')
|
||||
})
|
||||
|
||||
it('should handle empty voice value', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ voice: '' })
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
expect(selects[1]).toHaveAttribute('data-value', '')
|
||||
})
|
||||
|
||||
it('should handle many voices', () => {
|
||||
// Arrange
|
||||
const manyVoices = Array.from({ length: 20 }, (_, i) => ({
|
||||
mode: `voice-${i}`,
|
||||
name: `Voice ${i}`,
|
||||
}))
|
||||
const props = createDefaultProps({
|
||||
currentModel: createCurrentModel(manyVoices),
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('select-item-voice-0')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('select-item-voice-19')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle voice with special characters in mode', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({
|
||||
currentModel: createCurrentModel([
|
||||
{ mode: 'voice-with_special.chars', name: 'Special Voice' },
|
||||
]),
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('select-item-voice-with_special.chars')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle onChange not being called multiple times', () => {
|
||||
// Arrange
|
||||
const onChange = vi.fn()
|
||||
const props = createDefaultProps({ onChange })
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
fireEvent.click(screen.getByTestId('select-item-echo'))
|
||||
|
||||
// Assert
|
||||
expect(onChange).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Re-render Behavior ====================
|
||||
describe('Re-render Behavior', () => {
|
||||
it('should update when language prop changes', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ language: 'en-US' })
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<TTSParamsPanel {...props} />)
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
expect(selects[0]).toHaveAttribute('data-value', 'en-US')
|
||||
|
||||
rerender(<TTSParamsPanel {...props} language="zh-Hans" />)
|
||||
|
||||
// Assert
|
||||
const updatedSelects = screen.getAllByTestId('portal-select')
|
||||
expect(updatedSelects[0]).toHaveAttribute('data-value', 'zh-Hans')
|
||||
})
|
||||
|
||||
it('should update when voice prop changes', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ voice: 'alloy' })
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<TTSParamsPanel {...props} />)
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
expect(selects[1]).toHaveAttribute('data-value', 'alloy')
|
||||
|
||||
rerender(<TTSParamsPanel {...props} voice="echo" />)
|
||||
|
||||
// Assert
|
||||
const updatedSelects = screen.getAllByTestId('portal-select')
|
||||
expect(updatedSelects[1]).toHaveAttribute('data-value', 'echo')
|
||||
})
|
||||
|
||||
it('should update voice list when currentModel changes', () => {
|
||||
// Arrange
|
||||
const initialModel = createCurrentModel([
|
||||
{ mode: 'alloy', name: 'Alloy' },
|
||||
])
|
||||
const props = createDefaultProps({ currentModel: initialModel })
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<TTSParamsPanel {...props} />)
|
||||
expect(screen.getByTestId('select-item-alloy')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('select-item-nova')).not.toBeInTheDocument()
|
||||
|
||||
const newModel = createCurrentModel([
|
||||
{ mode: 'alloy', name: 'Alloy' },
|
||||
{ mode: 'nova', name: 'Nova' },
|
||||
])
|
||||
rerender(<TTSParamsPanel {...props} currentModel={newModel} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('select-item-alloy')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('select-item-nova')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle currentModel becoming null', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<TTSParamsPanel {...props} />)
|
||||
expect(screen.getByTestId('select-item-alloy')).toBeInTheDocument()
|
||||
|
||||
rerender(<TTSParamsPanel {...props} currentModel={null} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByTestId('select-item-alloy')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Component Type ====================
|
||||
describe('Component Type', () => {
|
||||
it('should be a functional component', () => {
|
||||
// Assert
|
||||
expect(typeof TTSParamsPanel).toBe('function')
|
||||
})
|
||||
|
||||
it('should accept all required props', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act & Assert
|
||||
expect(() => render(<TTSParamsPanel {...props} />)).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Accessibility ====================
|
||||
describe('Accessibility', () => {
|
||||
it('should have proper label structure for language select', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const languageLabel = screen.getByText('appDebug.voice.voiceSettings.language')
|
||||
expect(languageLabel).toHaveClass('system-sm-semibold')
|
||||
})
|
||||
|
||||
it('should have proper label structure for voice select', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const voiceLabel = screen.getByText('appDebug.voice.voiceSettings.voice')
|
||||
expect(voiceLabel).toHaveClass('system-sm-semibold')
|
||||
})
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue