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 }) => (
), })) // 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 }) => (
{parameterRule.label.en_US}
), })) // ==================== Test Utilities ==================== /** * Factory function to create a ModelParameterRule with defaults */ const createParameterRule = (overrides: Partial = {}): 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() // Assert expect(container).toBeInTheDocument() }) it('should render loading state when isPending is true', () => { // Arrange setupModelParameterRulesMock({ isPending: true }) const props = createDefaultProps() // Act render() // 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() // 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() // 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() // 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() // 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() // 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() // 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() // 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() // 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() // 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() // 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() // 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() // 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() 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() 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() 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() 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() 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() 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() 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() 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() // 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() // 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() // 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() // 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() // 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() // 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() 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() // 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() expect(screen.getByTestId('parameter-item-temperature')).toBeInTheDocument() // Update to loading setupModelParameterRulesMock({ isPending: true }) rerender() // 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() expect(screen.queryByTestId('parameter-item-stop')).not.toBeInTheDocument() rerender() // 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()).not.toThrow() }) }) })