Merge pull request #6560 from menloresearch/fix/layout-downlod-management
fix: download management ui and double refresh model
This commit is contained in:
commit
5b59c7a18a
@ -400,20 +400,23 @@ export function DownloadManagement() {
|
||||
className="text-main-view-fg/70 cursor-pointer"
|
||||
title="Cancel download"
|
||||
onClick={() => {
|
||||
serviceHub.models().abortDownload(download.name).then(() => {
|
||||
toast.info(
|
||||
t('common:toast.downloadCancelled.title'),
|
||||
{
|
||||
id: 'cancel-download',
|
||||
description: t(
|
||||
'common:toast.downloadCancelled.description'
|
||||
),
|
||||
serviceHub
|
||||
.models()
|
||||
.abortDownload(download.name)
|
||||
.then(() => {
|
||||
toast.info(
|
||||
t('common:toast.downloadCancelled.title'),
|
||||
{
|
||||
id: 'cancel-download',
|
||||
description: t(
|
||||
'common:toast.downloadCancelled.description'
|
||||
),
|
||||
}
|
||||
)
|
||||
if (downloadProcesses.length === 0) {
|
||||
setIsPopoverOpen(false)
|
||||
}
|
||||
)
|
||||
if (downloadProcesses.length === 0) {
|
||||
setIsPopoverOpen(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -35,7 +35,7 @@ import { toast } from 'sonner'
|
||||
import { DownloadManagement } from '@/containers/DownloadManegement'
|
||||
import { useSmallScreen } from '@/hooks/useMediaQuery'
|
||||
import { useClickOutside } from '@/hooks/useClickOutside'
|
||||
import { useDownloadStore } from '@/hooks/useDownloadStore'
|
||||
|
||||
import { DeleteAllThreadsDialog } from '@/containers/dialogs'
|
||||
|
||||
const mainMenus = [
|
||||
@ -122,7 +122,7 @@ const LeftPanel = () => {
|
||||
) {
|
||||
if (currentIsSmallScreen && open) {
|
||||
setLeftPanel(false)
|
||||
} else if(!open) {
|
||||
} else if (!open) {
|
||||
setLeftPanel(true)
|
||||
}
|
||||
prevScreenSizeRef.current = currentIsSmallScreen
|
||||
@ -179,8 +179,6 @@ const LeftPanel = () => {
|
||||
}
|
||||
}, [isSmallScreen, open])
|
||||
|
||||
const { downloads, localDownloadingModels } = useDownloadStore()
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Backdrop overlay for small screens */}
|
||||
@ -262,15 +260,8 @@ const LeftPanel = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col justify-between overflow-hidden mt-0 !h-[calc(100%-42px)]">
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col',
|
||||
Object.keys(downloads).length > 0 || localDownloadingModels.size > 0
|
||||
? 'h-[calc(100%-200px)]'
|
||||
: 'h-[calc(100%-140px)]'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col justify-between overflow-hidden mt-0 !h-[calc(100%-42px)] ">
|
||||
<div className={cn('flex flex-col !h-[calc(100%-200px)]')}>
|
||||
{IS_MACOS && (
|
||||
<div
|
||||
ref={searchContainerMacRef}
|
||||
@ -379,7 +370,9 @@ const LeftPanel = () => {
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="end">
|
||||
<DeleteAllThreadsDialog onDeleteAll={deleteAllThreads} />
|
||||
<DeleteAllThreadsDialog
|
||||
onDeleteAll={deleteAllThreads}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
@ -469,8 +462,9 @@ const LeftPanel = () => {
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
<DownloadManagement />
|
||||
</div>
|
||||
|
||||
<DownloadManagement />
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
|
||||
@ -7,8 +7,15 @@ import { cn } from '@/lib/utils'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
// Hook for the dropdown position
|
||||
function useDropdownPosition(open: boolean, containerRef: React.RefObject<HTMLDivElement | null>) {
|
||||
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 })
|
||||
function useDropdownPosition(
|
||||
open: boolean,
|
||||
containerRef: React.RefObject<HTMLDivElement | null>
|
||||
) {
|
||||
const [dropdownPosition, setDropdownPosition] = useState({
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 0,
|
||||
})
|
||||
|
||||
const updateDropdownPosition = useCallback(() => {
|
||||
if (containerRef.current) {
|
||||
@ -51,10 +58,18 @@ function useDropdownPosition(open: boolean, containerRef: React.RefObject<HTMLDi
|
||||
}
|
||||
|
||||
// Components for the different sections of the dropdown
|
||||
const ErrorSection = ({ error, t }: { error: string; t: (key: string) => string }) => (
|
||||
const ErrorSection = ({
|
||||
error,
|
||||
t,
|
||||
}: {
|
||||
error: string
|
||||
t: (key: string) => string
|
||||
}) => (
|
||||
<div className="px-3 py-2 text-sm text-destructive">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-destructive font-medium">{t('common:failedToLoadModels')}</span>
|
||||
<span className="text-destructive font-medium">
|
||||
{t('common:failedToLoadModels')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-main-view-fg/50 mt-0">{error}</div>
|
||||
</div>
|
||||
@ -67,12 +82,20 @@ const LoadingSection = ({ t }: { t: (key: string) => string }) => (
|
||||
</div>
|
||||
)
|
||||
|
||||
const EmptySection = ({ inputValue, t }: { inputValue: string; t: (key: string, options?: Record<string, string>) => string }) => (
|
||||
const EmptySection = ({
|
||||
inputValue,
|
||||
t,
|
||||
}: {
|
||||
inputValue: string
|
||||
t: (key: string, options?: Record<string, string>) => string
|
||||
}) => (
|
||||
<div className="px-3 py-3 text-sm text-main-view-fg/50 text-center">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
{inputValue.trim() ? (
|
||||
<span className="text-main-view-fg/50">{t('common:noModelsFoundFor', { searchValue: inputValue })}</span>
|
||||
<span className="text-main-view-fg/50">
|
||||
{t('common:noModelsFoundFor', { searchValue: inputValue })}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-main-view-fg/50">{t('common:noModels')}</span>
|
||||
)}
|
||||
@ -86,7 +109,7 @@ const ModelsList = ({
|
||||
value,
|
||||
highlightedIndex,
|
||||
onModelSelect,
|
||||
onHighlight
|
||||
onHighlight,
|
||||
}: {
|
||||
filteredModels: string[]
|
||||
value: string
|
||||
@ -127,67 +150,95 @@ function useKeyboardNavigation(
|
||||
onModelSelect: (model: string) => void,
|
||||
dropdownRef: React.RefObject<HTMLDivElement | null>
|
||||
) {
|
||||
|
||||
// Scroll to the highlighted element
|
||||
useEffect(() => {
|
||||
if (highlightedIndex >= 0 && dropdownRef.current) {
|
||||
requestAnimationFrame(() => {
|
||||
const modelElements = dropdownRef.current?.querySelectorAll('[data-model]')
|
||||
const highlightedElement = modelElements?.[highlightedIndex] as HTMLElement
|
||||
const modelElements =
|
||||
dropdownRef.current?.querySelectorAll('[data-model]')
|
||||
const highlightedElement = modelElements?.[
|
||||
highlightedIndex
|
||||
] as HTMLElement
|
||||
if (highlightedElement) {
|
||||
highlightedElement.scrollIntoView({
|
||||
block: 'nearest',
|
||||
behavior: 'auto'
|
||||
behavior: 'auto',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [highlightedIndex, dropdownRef])
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
// Open the dropdown with the arrows if closed
|
||||
if (!open && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
|
||||
if (models.length > 0) {
|
||||
e.preventDefault()
|
||||
setOpen(true)
|
||||
setHighlightedIndex(0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!open) return
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
setHighlightedIndex((prev: number) => filteredModels.length === 0 ? 0 : (prev < filteredModels.length - 1 ? prev + 1 : 0))
|
||||
break
|
||||
case 'ArrowUp':
|
||||
e.preventDefault()
|
||||
setHighlightedIndex((prev: number) => filteredModels.length === 0 ? 0 : (prev > 0 ? prev - 1 : filteredModels.length - 1))
|
||||
break
|
||||
case 'Enter':
|
||||
e.preventDefault()
|
||||
if (highlightedIndex >= 0 && highlightedIndex < filteredModels.length) {
|
||||
onModelSelect(filteredModels[highlightedIndex])
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
// Open the dropdown with the arrows if closed
|
||||
if (!open && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
|
||||
if (models.length > 0) {
|
||||
e.preventDefault()
|
||||
setOpen(true)
|
||||
setHighlightedIndex(0)
|
||||
}
|
||||
break
|
||||
case 'Escape':
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setOpen(false)
|
||||
setHighlightedIndex(-1)
|
||||
break
|
||||
case 'PageUp':
|
||||
e.preventDefault()
|
||||
setHighlightedIndex(0)
|
||||
break
|
||||
case 'PageDown':
|
||||
e.preventDefault()
|
||||
setHighlightedIndex(filteredModels.length - 1)
|
||||
break
|
||||
}
|
||||
}, [open, setOpen, models.length, filteredModels, highlightedIndex, setHighlightedIndex, onModelSelect])
|
||||
return
|
||||
}
|
||||
|
||||
if (!open) return
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
setHighlightedIndex((prev: number) =>
|
||||
filteredModels.length === 0
|
||||
? 0
|
||||
: prev < filteredModels.length - 1
|
||||
? prev + 1
|
||||
: 0
|
||||
)
|
||||
break
|
||||
case 'ArrowUp':
|
||||
e.preventDefault()
|
||||
setHighlightedIndex((prev: number) =>
|
||||
filteredModels.length === 0
|
||||
? 0
|
||||
: prev > 0
|
||||
? prev - 1
|
||||
: filteredModels.length - 1
|
||||
)
|
||||
break
|
||||
case 'Enter':
|
||||
e.preventDefault()
|
||||
if (
|
||||
highlightedIndex >= 0 &&
|
||||
highlightedIndex < filteredModels.length
|
||||
) {
|
||||
onModelSelect(filteredModels[highlightedIndex])
|
||||
}
|
||||
break
|
||||
case 'Escape':
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setOpen(false)
|
||||
setHighlightedIndex(-1)
|
||||
break
|
||||
case 'PageUp':
|
||||
e.preventDefault()
|
||||
setHighlightedIndex(0)
|
||||
break
|
||||
case 'PageDown':
|
||||
e.preventDefault()
|
||||
setHighlightedIndex(filteredModels.length - 1)
|
||||
break
|
||||
}
|
||||
},
|
||||
[
|
||||
open,
|
||||
setOpen,
|
||||
models.length,
|
||||
filteredModels,
|
||||
highlightedIndex,
|
||||
setHighlightedIndex,
|
||||
onModelSelect,
|
||||
]
|
||||
)
|
||||
|
||||
return { handleKeyDown }
|
||||
}
|
||||
@ -266,13 +317,18 @@ export function ModelCombobox({
|
||||
}
|
||||
|
||||
const events = ['mousedown', 'touchstart']
|
||||
events.forEach(eventType => {
|
||||
document.addEventListener(eventType, handleClickOutside, { capture: true, passive: true })
|
||||
events.forEach((eventType) => {
|
||||
document.addEventListener(eventType, handleClickOutside, {
|
||||
capture: true,
|
||||
passive: true,
|
||||
})
|
||||
})
|
||||
|
||||
return () => {
|
||||
events.forEach(eventType => {
|
||||
document.removeEventListener(eventType, handleClickOutside, { capture: true })
|
||||
events.forEach((eventType) => {
|
||||
document.removeEventListener(eventType, handleClickOutside, {
|
||||
capture: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
}, [open])
|
||||
@ -286,26 +342,32 @@ export function ModelCombobox({
|
||||
}, [])
|
||||
|
||||
// Handler for the input change
|
||||
const handleInputChange = useCallback((newValue: string) => {
|
||||
setInputValue(newValue)
|
||||
onChange(newValue)
|
||||
const handleInputChange = useCallback(
|
||||
(newValue: string) => {
|
||||
setInputValue(newValue)
|
||||
onChange(newValue)
|
||||
|
||||
// Open the dropdown if the user types and there are models
|
||||
if (newValue.trim() && models.length > 0) {
|
||||
setOpen(true)
|
||||
} else {
|
||||
setOpen(false)
|
||||
}
|
||||
}, [onChange, models.length])
|
||||
// Open the dropdown if the user types and there are models
|
||||
if (newValue.trim() && models.length > 0) {
|
||||
setOpen(true)
|
||||
} else {
|
||||
setOpen(false)
|
||||
}
|
||||
},
|
||||
[onChange, models.length]
|
||||
)
|
||||
|
||||
// Handler for the model selection
|
||||
const handleModelSelect = useCallback((model: string) => {
|
||||
setInputValue(model)
|
||||
onChange(model)
|
||||
setOpen(false)
|
||||
setHighlightedIndex(-1)
|
||||
inputRef.current?.focus()
|
||||
}, [onChange])
|
||||
const handleModelSelect = useCallback(
|
||||
(model: string) => {
|
||||
setInputValue(model)
|
||||
onChange(model)
|
||||
setOpen(false)
|
||||
setHighlightedIndex(-1)
|
||||
inputRef.current?.focus()
|
||||
},
|
||||
[onChange]
|
||||
)
|
||||
|
||||
// Hook for the keyboard navigation
|
||||
const { handleKeyDown } = useKeyboardNavigation(
|
||||
@ -376,54 +438,52 @@ export function ModelCombobox({
|
||||
onClick={handleDropdownToggle}
|
||||
className="h-6 w-6 p-0 no-underline hover:bg-main-view-fg/10"
|
||||
>
|
||||
{loading ? (
|
||||
<IconLoader2 className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<IconChevronDown className="h-3 w-3 opacity-50" />
|
||||
)}
|
||||
<IconChevronDown className="h-3 w-3 opacity-50" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Custom dropdown rendered as portal */}
|
||||
{open && dropdownPosition.width > 0 && createPortal(
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className="fixed z-[9999] bg-main-view border border-main-view-fg/10 rounded-md shadow-lg max-h-[300px] overflow-y-auto text-main-view-fg animate-in fade-in-0 zoom-in-95 duration-200"
|
||||
style={{
|
||||
top: dropdownPosition.top,
|
||||
left: dropdownPosition.left,
|
||||
width: dropdownPosition.width,
|
||||
minWidth: dropdownPosition.width,
|
||||
maxWidth: dropdownPosition.width,
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
data-dropdown="model-combobox"
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onWheel={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Error state */}
|
||||
{error && <ErrorSection error={error} t={t} />}
|
||||
{open &&
|
||||
dropdownPosition.width > 0 &&
|
||||
createPortal(
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className="fixed z-[9999] bg-main-view border border-main-view-fg/10 rounded-md shadow-lg max-h-[300px] overflow-y-auto text-main-view-fg animate-in fade-in-0 zoom-in-95 duration-200"
|
||||
style={{
|
||||
top: dropdownPosition.top,
|
||||
left: dropdownPosition.left,
|
||||
width: dropdownPosition.width,
|
||||
minWidth: dropdownPosition.width,
|
||||
maxWidth: dropdownPosition.width,
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
data-dropdown="model-combobox"
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onWheel={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Error state */}
|
||||
{error && <ErrorSection error={error} t={t} />}
|
||||
|
||||
{/* Loading state */}
|
||||
{loading && <LoadingSection t={t} />}
|
||||
{/* Loading state */}
|
||||
{loading && <LoadingSection t={t} />}
|
||||
|
||||
{/* Models list */}
|
||||
{!loading && !error && (
|
||||
filteredModels.length === 0 ? (
|
||||
<EmptySection inputValue={inputValue} t={t} />
|
||||
) : (
|
||||
<ModelsList
|
||||
filteredModels={filteredModels}
|
||||
value={value}
|
||||
highlightedIndex={highlightedIndex}
|
||||
onModelSelect={handleModelSelect}
|
||||
onHighlight={setHighlightedIndex}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
{/* Models list */}
|
||||
{!loading &&
|
||||
!error &&
|
||||
(filteredModels.length === 0 ? (
|
||||
<EmptySection inputValue={inputValue} t={t} />
|
||||
) : (
|
||||
<ModelsList
|
||||
filteredModels={filteredModels}
|
||||
value={value}
|
||||
highlightedIndex={highlightedIndex}
|
||||
onModelSelect={handleModelSelect}
|
||||
onHighlight={setHighlightedIndex}
|
||||
/>
|
||||
))}
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,4 +1,12 @@
|
||||
import { describe, it, expect, vi, beforeEach, beforeAll, afterAll } from 'vitest'
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
beforeAll,
|
||||
afterAll,
|
||||
} 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'
|
||||
@ -11,7 +19,8 @@ vi.mock('@/i18n/react-i18next-compat', () => ({
|
||||
t: (key: string, options?: Record<string, string>) => {
|
||||
if (key === 'common:failedToLoadModels') return 'Failed to load models'
|
||||
if (key === 'common:loading') return 'Loading'
|
||||
if (key === 'common:noModelsFoundFor') return `No models found for "${options?.searchValue}"`
|
||||
if (key === 'common:noModelsFoundFor')
|
||||
return `No models found for "${options?.searchValue}"`
|
||||
if (key === 'common:noModels') return 'No models available'
|
||||
return key
|
||||
},
|
||||
@ -21,7 +30,7 @@ vi.mock('@/i18n/react-i18next-compat', () => ({
|
||||
describe('ModelCombobox', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
const mockOnRefresh = vi.fn()
|
||||
|
||||
|
||||
const defaultProps = {
|
||||
value: '',
|
||||
onChange: mockOnChange,
|
||||
@ -64,7 +73,7 @@ describe('ModelCombobox', () => {
|
||||
act(() => {
|
||||
render(<ModelCombobox {...defaultProps} />)
|
||||
})
|
||||
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toHaveAttribute('placeholder', 'Type or select a model...')
|
||||
@ -74,7 +83,7 @@ describe('ModelCombobox', () => {
|
||||
act(() => {
|
||||
render(<ModelCombobox {...defaultProps} placeholder="Choose a model" />)
|
||||
})
|
||||
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
expect(input).toHaveAttribute('placeholder', 'Choose a model')
|
||||
})
|
||||
@ -83,7 +92,7 @@ describe('ModelCombobox', () => {
|
||||
act(() => {
|
||||
render(<ModelCombobox {...defaultProps} />)
|
||||
})
|
||||
|
||||
|
||||
const button = screen.getByRole('button')
|
||||
expect(button).toBeInTheDocument()
|
||||
})
|
||||
@ -92,7 +101,7 @@ describe('ModelCombobox', () => {
|
||||
act(() => {
|
||||
render(<ModelCombobox {...defaultProps} value="gpt-4" />)
|
||||
})
|
||||
|
||||
|
||||
const input = screen.getByDisplayValue('gpt-4')
|
||||
expect(input).toBeInTheDocument()
|
||||
})
|
||||
@ -110,7 +119,7 @@ describe('ModelCombobox', () => {
|
||||
act(() => {
|
||||
render(<ModelCombobox {...defaultProps} disabled />)
|
||||
})
|
||||
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
const button = screen.getByRole('button')
|
||||
|
||||
@ -118,27 +127,19 @@ describe('ModelCombobox', () => {
|
||||
expect(button).toBeDisabled()
|
||||
})
|
||||
|
||||
it('shows loading spinner in trigger button', () => {
|
||||
act(() => {
|
||||
render(<ModelCombobox {...defaultProps} loading />)
|
||||
})
|
||||
|
||||
const button = screen.getByRole('button')
|
||||
const spinner = button.querySelector('.animate-spin')
|
||||
expect(spinner).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows loading section when dropdown is opened during loading', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<ModelCombobox {...defaultProps} loading />)
|
||||
|
||||
|
||||
// Click input to trigger dropdown opening
|
||||
const input = screen.getByRole('textbox')
|
||||
await user.click(input)
|
||||
|
||||
|
||||
// Wait for dropdown to appear and check loading section
|
||||
await waitFor(() => {
|
||||
const dropdown = document.querySelector('[data-dropdown="model-combobox"]')
|
||||
const dropdown = document.querySelector(
|
||||
'[data-dropdown="model-combobox"]'
|
||||
)
|
||||
expect(dropdown).toBeInTheDocument()
|
||||
expect(screen.getByText('Loading')).toBeInTheDocument()
|
||||
})
|
||||
@ -179,7 +180,7 @@ describe('ModelCombobox', () => {
|
||||
act(() => {
|
||||
render(<ModelCombobox {...defaultProps} models={[]} />)
|
||||
})
|
||||
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
expect(input).toBeInTheDocument()
|
||||
})
|
||||
@ -188,7 +189,7 @@ describe('ModelCombobox', () => {
|
||||
act(() => {
|
||||
render(<ModelCombobox {...defaultProps} models={['model1', 'model2']} />)
|
||||
})
|
||||
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
expect(input).toBeInTheDocument()
|
||||
})
|
||||
@ -259,7 +260,7 @@ describe('ModelCombobox', () => {
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toBeDisabled()
|
||||
@ -273,7 +274,9 @@ describe('ModelCombobox', () => {
|
||||
await user.click(button)
|
||||
|
||||
await waitFor(() => {
|
||||
const dropdown = document.querySelector('[data-dropdown="model-combobox"]')
|
||||
const dropdown = document.querySelector(
|
||||
'[data-dropdown="model-combobox"]'
|
||||
)
|
||||
expect(dropdown).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -287,7 +290,9 @@ describe('ModelCombobox', () => {
|
||||
|
||||
expect(input).toHaveFocus()
|
||||
await waitFor(() => {
|
||||
const dropdown = document.querySelector('[data-dropdown="model-combobox"]')
|
||||
const dropdown = document.querySelector(
|
||||
'[data-dropdown="model-combobox"]'
|
||||
)
|
||||
expect(dropdown).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -313,9 +318,11 @@ describe('ModelCombobox', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
// Dropdown should be open
|
||||
const dropdown = document.querySelector('[data-dropdown="model-combobox"]')
|
||||
const dropdown = document.querySelector(
|
||||
'[data-dropdown="model-combobox"]'
|
||||
)
|
||||
expect(dropdown).toBeInTheDocument()
|
||||
|
||||
|
||||
// Should show GPT models
|
||||
expect(screen.getByText('gpt-3.5-turbo')).toBeInTheDocument()
|
||||
expect(screen.getByText('gpt-4')).toBeInTheDocument()
|
||||
@ -344,10 +351,14 @@ describe('ModelCombobox', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
// Dropdown should be open
|
||||
const dropdown = document.querySelector('[data-dropdown="model-combobox"]')
|
||||
const dropdown = document.querySelector(
|
||||
'[data-dropdown="model-combobox"]'
|
||||
)
|
||||
expect(dropdown).toBeInTheDocument()
|
||||
// Should show empty state message
|
||||
expect(screen.getByText('No models found for "nonexistent"')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText('No models found for "nonexistent"')
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -358,12 +369,12 @@ describe('ModelCombobox', () => {
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
await user.click(input)
|
||||
|
||||
|
||||
await waitFor(() => {
|
||||
const modelOption = screen.getByText('gpt-4')
|
||||
expect(modelOption).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
const modelOption = screen.getByText('gpt-4')
|
||||
await user.click(modelOption)
|
||||
|
||||
@ -385,7 +396,9 @@ describe('ModelCombobox', () => {
|
||||
|
||||
it('displays error message in dropdown', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<ModelCombobox {...defaultProps} error="Network connection failed" />)
|
||||
render(
|
||||
<ModelCombobox {...defaultProps} error="Network connection failed" />
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
// Click input to open dropdown
|
||||
@ -393,7 +406,9 @@ describe('ModelCombobox', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
// Dropdown should be open
|
||||
const dropdown = document.querySelector('[data-dropdown="model-combobox"]')
|
||||
const dropdown = document.querySelector(
|
||||
'[data-dropdown="model-combobox"]'
|
||||
)
|
||||
expect(dropdown).toBeInTheDocument()
|
||||
// Error messages should be displayed
|
||||
expect(screen.getByText('Failed to load models')).toBeInTheDocument()
|
||||
@ -404,7 +419,13 @@ describe('ModelCombobox', () => {
|
||||
it('calls onRefresh when refresh button is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
const localMockOnRefresh = vi.fn()
|
||||
render(<ModelCombobox {...defaultProps} error="Network error" onRefresh={localMockOnRefresh} />)
|
||||
render(
|
||||
<ModelCombobox
|
||||
{...defaultProps}
|
||||
error="Network error"
|
||||
onRefresh={localMockOnRefresh}
|
||||
/>
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
// Click input to open dropdown
|
||||
@ -412,13 +433,19 @@ describe('ModelCombobox', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
// Dropdown should be open with error section
|
||||
const dropdown = document.querySelector('[data-dropdown="model-combobox"]')
|
||||
const dropdown = document.querySelector(
|
||||
'[data-dropdown="model-combobox"]'
|
||||
)
|
||||
expect(dropdown).toBeInTheDocument()
|
||||
const refreshButton = document.querySelector('[aria-label="Refresh models"]')
|
||||
const refreshButton = document.querySelector(
|
||||
'[aria-label="Refresh models"]'
|
||||
)
|
||||
expect(refreshButton).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const refreshButton = document.querySelector('[aria-label="Refresh models"]')
|
||||
const refreshButton = document.querySelector(
|
||||
'[aria-label="Refresh models"]'
|
||||
)
|
||||
if (refreshButton) {
|
||||
await user.click(refreshButton)
|
||||
expect(localMockOnRefresh).toHaveBeenCalledTimes(1)
|
||||
@ -435,7 +462,9 @@ describe('ModelCombobox', () => {
|
||||
|
||||
expect(input).toHaveFocus()
|
||||
await waitFor(() => {
|
||||
const dropdown = document.querySelector('[data-dropdown="model-combobox"]')
|
||||
const dropdown = document.querySelector(
|
||||
'[data-dropdown="model-combobox"]'
|
||||
)
|
||||
expect(dropdown).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -446,16 +475,18 @@ describe('ModelCombobox', () => {
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
input.focus()
|
||||
|
||||
|
||||
// ArrowDown should open dropdown
|
||||
await user.keyboard('{ArrowDown}')
|
||||
|
||||
|
||||
await waitFor(() => {
|
||||
// Dropdown should be open
|
||||
const dropdown = document.querySelector('[data-dropdown="model-combobox"]')
|
||||
const dropdown = document.querySelector(
|
||||
'[data-dropdown="model-combobox"]'
|
||||
)
|
||||
expect(dropdown).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
// Navigate to second item
|
||||
await user.keyboard('{ArrowDown}')
|
||||
|
||||
@ -474,13 +505,15 @@ describe('ModelCombobox', () => {
|
||||
const input = screen.getByRole('textbox')
|
||||
// Type 'gpt' to open dropdown and filter models
|
||||
await user.type(input, 'gpt')
|
||||
|
||||
|
||||
await waitFor(() => {
|
||||
// Dropdown should be open with filtered models
|
||||
const dropdown = document.querySelector('[data-dropdown="model-combobox"]')
|
||||
const dropdown = document.querySelector(
|
||||
'[data-dropdown="model-combobox"]'
|
||||
)
|
||||
expect(dropdown).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
// Navigate to highlight first model and select it
|
||||
await user.keyboard('{ArrowDown}')
|
||||
await user.keyboard('{Enter}')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user