Files
aiturk-hermes-ide/apps/desktop/src/app/settings/searchable-select.test.tsx
T

132 lines
4.6 KiB
TypeScript

import { cleanup, fireEvent, screen } from '@testing-library/react'
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
import { stubResizeObserver } from '@/test/jsdom'
import type { ConfigFieldSchema } from '@/types/hermes'
import { ConfigField } from './config-field'
import { rankSearchOption, SearchableSelect } from './searchable-select'
import { renderWithEnglish as render } from './test-locale'
beforeAll(() => {
stubResizeObserver()
Element.prototype.scrollIntoView = vi.fn()
Element.prototype.hasPointerCapture = vi.fn(() => false)
Element.prototype.releasePointerCapture = vi.fn()
})
afterEach(() => {
cleanup()
vi.clearAllMocks()
})
describe('rankSearchOption', () => {
it('ranks a final-segment match above a mid-path match', () => {
// "york" hits the city segment of America/New_York (score 2) but only a
// mid-path segment of America/New_York/Special (score 1).
expect(rankSearchOption('America/New_York', 'york')).toBe(2)
expect(rankSearchOption('America/New_York/Special', 'york')).toBe(1)
expect(rankSearchOption('America/New_York', 'york')).toBeGreaterThan(
rankSearchOption('America/New_York/Special', 'york')
)
})
it('is case-insensitive', () => {
expect(rankSearchOption('Asia/Kolkata', 'KOLKATA')).toBe(2)
expect(rankSearchOption('ASIA/KOLKATA', 'kolkata')).toBe(2)
})
it('scores a substring match anywhere as 1', () => {
expect(rankSearchOption('America/New_York', 'amer')).toBe(1)
})
it('scores a slashless option by plain substring', () => {
expect(rankSearchOption('UTC', 'ut')).toBe(1)
expect(rankSearchOption('UTC', 'xyz')).toBe(0)
})
it('scores a non-match as 0', () => {
expect(rankSearchOption('Europe/Berlin', 'tokyo')).toBe(0)
})
})
describe('SearchableSelect', () => {
const options = ['America/New_York', 'Asia/Kolkata', 'Europe/Berlin', 'UTC']
it('opens, filters, and selects an option', () => {
const onChange = vi.fn()
render(<SearchableSelect onChange={onChange} options={options} placeholder="Search…" value="" />)
fireEvent.click(screen.getByRole('combobox'))
fireEvent.change(screen.getByPlaceholderText('Search…'), { target: { value: 'kolkata' } })
fireEvent.click(screen.getByText('Asia/Kolkata'))
expect(onChange).toHaveBeenCalledWith('Asia/Kolkata')
})
it('renders the clear item when clearLabel is set and selecting it resets to blank', () => {
const onChange = vi.fn()
render(<SearchableSelect clearLabel="System default" onChange={onChange} options={options} value="Asia/Kolkata" />)
fireEvent.click(screen.getByRole('combobox'))
fireEvent.click(screen.getByText('System default'))
expect(onChange).toHaveBeenCalledWith('')
})
it('omits the clear item without clearLabel', () => {
render(<SearchableSelect onChange={vi.fn()} options={options} value="" />)
fireEvent.click(screen.getByRole('combobox'))
expect(screen.queryByText('System default')).toBeNull()
})
it('shows the placeholder when the value is blank', () => {
render(<SearchableSelect onChange={vi.fn()} options={options} placeholder="Search…" value="" />)
expect(screen.getByRole('combobox').textContent).toContain('Search…')
})
})
describe('ConfigField searchable routing', () => {
const searchableSchema: ConfigFieldSchema = {
type: 'select',
searchable: true,
clearable: true,
options: ['America/New_York', 'UTC']
}
it('routes searchable select schemas to SearchableSelect, not a free-text input', () => {
const { container } = render(
<ConfigField onChange={vi.fn()} schema={searchableSchema} schemaKey="timezone" value="UTC" />
)
// The searchable trigger renders; the generic free-text <Input> does not.
expect(container.querySelector('[data-slot="searchable-select-trigger"]')).not.toBeNull()
expect(container.querySelector('input[type="text"]')).toBeNull()
})
it('keeps plain string schemas on the free-text input', () => {
const { container } = render(
<ConfigField onChange={vi.fn()} schema={{ type: 'string' }} schemaKey="some.other.key" value="hello" />
)
expect(container.querySelector('[data-slot="searchable-select-trigger"]')).toBeNull()
expect(screen.getByDisplayValue('hello')).not.toBeNull()
})
it('surfaces the clear item via schema.clearable and resets to blank', () => {
const onChange = vi.fn()
render(<ConfigField onChange={onChange} schema={searchableSchema} schemaKey="timezone" value="UTC" />)
fireEvent.click(screen.getByRole('combobox'))
fireEvent.click(screen.getByText('System default'))
expect(onChange).toHaveBeenCalledWith('')
})
})