test: add unit tests for ModelCombobox, useProviderModels and providers
This commit is contained in:
parent
5d9c3ab462
commit
3339629747
197
web-app/src/containers/__tests__/ModelCombobox.test.tsx
Normal file
197
web-app/src/containers/__tests__/ModelCombobox.test.tsx
Normal file
@ -0,0 +1,197 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
import { ModelCombobox } from '../ModelCombobox'
|
||||
import React from 'react'
|
||||
|
||||
describe('ModelCombobox', () => {
|
||||
const defaultProps = {
|
||||
value: '',
|
||||
onChange: vi.fn(),
|
||||
models: ['gpt-3.5-turbo', 'gpt-4', 'claude-3-haiku'],
|
||||
}
|
||||
|
||||
const mockUser = userEvent.setup()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('Basic Rendering', () => {
|
||||
it('should render input field with placeholder', () => {
|
||||
render(<ModelCombobox {...defaultProps} />)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toHaveAttribute('placeholder', 'Type or select a model...')
|
||||
})
|
||||
|
||||
it('should render custom placeholder', () => {
|
||||
render(<ModelCombobox {...defaultProps} placeholder="Choose a model" />)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
expect(input).toHaveAttribute('placeholder', 'Choose a model')
|
||||
})
|
||||
|
||||
it('should render dropdown trigger button', () => {
|
||||
render(<ModelCombobox {...defaultProps} />)
|
||||
const button = screen.getByRole('button')
|
||||
expect(button).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display current value in input', () => {
|
||||
render(<ModelCombobox {...defaultProps} value="gpt-4" />)
|
||||
|
||||
const input = screen.getByDisplayValue('gpt-4')
|
||||
expect(input).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(
|
||||
<ModelCombobox {...defaultProps} className="custom-class" />
|
||||
)
|
||||
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveClass('custom-class')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Disabled State', () => {
|
||||
it('should disable input when disabled prop is true', () => {
|
||||
render(<ModelCombobox {...defaultProps} disabled />)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
const button = screen.getByRole('button')
|
||||
|
||||
expect(input).toBeDisabled()
|
||||
expect(button).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should not open dropdown when disabled', async () => {
|
||||
render(<ModelCombobox {...defaultProps} disabled />)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
await mockUser.click(input)
|
||||
|
||||
expect(screen.queryByTestId('dropdown')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Loading State', () => {
|
||||
it('should show loading spinner in trigger button', () => {
|
||||
render(<ModelCombobox {...defaultProps} loading />)
|
||||
|
||||
const button = screen.getByRole('button')
|
||||
const spinner = button.querySelector('.animate-spin')
|
||||
expect(spinner).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show loading spinner when loading prop is true', () => {
|
||||
render(<ModelCombobox {...defaultProps} loading />)
|
||||
|
||||
const spinner = screen.getByRole('button').querySelector('.animate-spin')
|
||||
expect(spinner).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Input Interactions', () => {
|
||||
it('should call onChange when typing', async () => {
|
||||
const mockOnChange = vi.fn()
|
||||
render(<ModelCombobox {...defaultProps} onChange={mockOnChange} />)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
await mockUser.type(input, 'g')
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith('g')
|
||||
})
|
||||
|
||||
it('should update input value when typing', async () => {
|
||||
const mockOnChange = vi.fn()
|
||||
render(<ModelCombobox {...defaultProps} onChange={mockOnChange} />)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
await mockUser.type(input, 'test')
|
||||
|
||||
expect(input).toHaveValue('test')
|
||||
})
|
||||
|
||||
it('should handle input focus', async () => {
|
||||
render(<ModelCombobox {...defaultProps} />)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
await mockUser.click(input)
|
||||
|
||||
expect(input).toHaveFocus()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props Validation', () => {
|
||||
it('should render with empty models array', () => {
|
||||
render(<ModelCombobox {...defaultProps} models={[]} />)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
expect(input).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with models array', () => {
|
||||
render(<ModelCombobox {...defaultProps} models={['model1', 'model2']} />)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
expect(input).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with all props', () => {
|
||||
render(
|
||||
<ModelCombobox
|
||||
{...defaultProps}
|
||||
loading
|
||||
error="Error message"
|
||||
onRefresh={vi.fn()}
|
||||
placeholder="Custom placeholder"
|
||||
disabled
|
||||
/>
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Component Lifecycle', () => {
|
||||
it('should handle mount and unmount without errors', () => {
|
||||
const { unmount } = render(<ModelCombobox {...defaultProps} />)
|
||||
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
|
||||
unmount()
|
||||
|
||||
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle props changes', () => {
|
||||
const { rerender } = render(<ModelCombobox {...defaultProps} value="" />)
|
||||
|
||||
expect(screen.getByDisplayValue('')).toBeInTheDocument()
|
||||
|
||||
rerender(<ModelCombobox {...defaultProps} value="gpt-4" />)
|
||||
|
||||
expect(screen.getByDisplayValue('gpt-4')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle models array changes', () => {
|
||||
const { rerender } = render(<ModelCombobox {...defaultProps} models={[]} />)
|
||||
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
|
||||
rerender(<ModelCombobox {...defaultProps} models={['model1', 'model2']} />)
|
||||
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
105
web-app/src/hooks/__tests__/useProviderModels.test.ts
Normal file
105
web-app/src/hooks/__tests__/useProviderModels.test.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { renderHook, waitFor } from '@testing-library/react'
|
||||
import { useProviderModels } from '../useProviderModels'
|
||||
|
||||
// Mock the providers service
|
||||
vi.mock('@/services/providers', () => ({
|
||||
fetchModelsFromProvider: vi.fn(),
|
||||
}))
|
||||
|
||||
import { fetchModelsFromProvider } from '@/services/providers'
|
||||
const mockFetchModelsFromProvider = vi.mocked(fetchModelsFromProvider)
|
||||
|
||||
// Mock ModelProvider type
|
||||
type MockModelProvider = {
|
||||
active: boolean
|
||||
provider: string
|
||||
base_url?: string
|
||||
api_key?: string
|
||||
settings: any[]
|
||||
models: any[]
|
||||
}
|
||||
|
||||
describe('useProviderModels', () => {
|
||||
const mockProvider: MockModelProvider = {
|
||||
active: true,
|
||||
provider: 'openai',
|
||||
base_url: 'https://api.openai.com/v1',
|
||||
api_key: 'test-api-key',
|
||||
settings: [],
|
||||
models: [],
|
||||
}
|
||||
|
||||
const mockModels = ['gpt-4', 'gpt-3.5-turbo', 'gpt-4-turbo']
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Reset the cache by clearing any previous state
|
||||
mockFetchModelsFromProvider.mockClear()
|
||||
})
|
||||
|
||||
it('should initialize with empty state', () => {
|
||||
const { result } = renderHook(() => useProviderModels())
|
||||
|
||||
expect(result.current.models).toEqual([])
|
||||
expect(result.current.loading).toBe(false)
|
||||
expect(result.current.error).toBe(null)
|
||||
expect(typeof result.current.refetch).toBe('function')
|
||||
})
|
||||
|
||||
it('should not fetch models when provider is undefined', () => {
|
||||
renderHook(() => useProviderModels(undefined))
|
||||
expect(mockFetchModelsFromProvider).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not fetch models when provider has no base_url', () => {
|
||||
const providerWithoutUrl = { ...mockProvider, base_url: undefined }
|
||||
renderHook(() => useProviderModels(providerWithoutUrl))
|
||||
expect(mockFetchModelsFromProvider).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should fetch and sort models', async () => {
|
||||
mockFetchModelsFromProvider.mockResolvedValueOnce(mockModels)
|
||||
|
||||
const { result } = renderHook(() => useProviderModels(mockProvider))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
// Should be sorted alphabetically
|
||||
expect(result.current.models).toEqual(['gpt-3.5-turbo', 'gpt-4', 'gpt-4-turbo'])
|
||||
expect(result.current.error).toBe(null)
|
||||
expect(mockFetchModelsFromProvider).toHaveBeenCalledWith(mockProvider)
|
||||
})
|
||||
|
||||
it('should clear models when switching to invalid provider', async () => {
|
||||
mockFetchModelsFromProvider.mockResolvedValueOnce(mockModels)
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
({ provider }) => useProviderModels(provider),
|
||||
{ initialProps: { provider: mockProvider } }
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
expect(result.current.models).toEqual(['gpt-3.5-turbo', 'gpt-4', 'gpt-4-turbo'])
|
||||
|
||||
// Switch to invalid provider
|
||||
rerender({ provider: { ...mockProvider, base_url: undefined } })
|
||||
|
||||
expect(result.current.models).toEqual([])
|
||||
expect(result.current.error).toBe(null)
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
it('should not refetch when provider is undefined', () => {
|
||||
const { result } = renderHook(() => useProviderModels(undefined))
|
||||
|
||||
result.current.refetch()
|
||||
|
||||
expect(mockFetchModelsFromProvider).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@ -222,7 +222,7 @@ describe('providers service', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw error when API response is not ok', async () => {
|
||||
it('should throw error when API response is not ok (404)', async () => {
|
||||
const mockResponse = {
|
||||
ok: false,
|
||||
status: 404,
|
||||
@ -236,7 +236,43 @@ describe('providers service', () => {
|
||||
}
|
||||
|
||||
await expect(fetchModelsFromProvider(provider)).rejects.toThrow(
|
||||
'Cannot connect to custom at https://api.custom.com. Please check that the service is running and accessible.'
|
||||
'Models endpoint not found for custom. Check the base URL configuration.'
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw error when API response is not ok (403)', async () => {
|
||||
const mockResponse = {
|
||||
ok: false,
|
||||
status: 403,
|
||||
statusText: 'Forbidden',
|
||||
}
|
||||
vi.mocked(fetchTauri).mockResolvedValue(mockResponse as any)
|
||||
|
||||
const provider = {
|
||||
provider: 'custom',
|
||||
base_url: 'https://api.custom.com',
|
||||
} as ModelProvider
|
||||
|
||||
await expect(fetchModelsFromProvider(provider)).rejects.toThrow(
|
||||
'Access forbidden: Check your API key permissions for custom'
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw error when API response is not ok (401)', async () => {
|
||||
const mockResponse = {
|
||||
ok: false,
|
||||
status: 401,
|
||||
statusText: 'Unauthorized',
|
||||
}
|
||||
vi.mocked(fetchTauri).mockResolvedValue(mockResponse as any)
|
||||
|
||||
const provider = {
|
||||
provider: 'custom',
|
||||
base_url: 'https://api.custom.com',
|
||||
} as ModelProvider
|
||||
|
||||
await expect(fetchModelsFromProvider(provider)).rejects.toThrow(
|
||||
'Authentication failed: API key is required or invalid for custom'
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user