@furystack/shades-common-components
Version:
Common UI components for FuryStack Shades
334 lines (263 loc) • 11.2 kB
text/typescript
import { using } from '@furystack/utils'
import { describe, expect, it, vi } from 'vitest'
import { ListService } from './list-service.js'
type TestItem = { id: number; name: string }
describe('ListService', () => {
const createTestService = (options?: ConstructorParameters<typeof ListService<TestItem>>[0]) => {
const service = new ListService<TestItem>(options)
const items: TestItem[] = [
{ id: 1, name: 'First' },
{ id: 2, name: 'Second' },
{ id: 3, name: 'Third' },
]
service.items.setValue(items)
return { service, items }
}
describe('selection helpers', () => {
it('should check if item is selected', () => {
const { service, items } = createTestService()
using(service, () => {
service.selection.setValue([items[0]])
expect(service.isSelected(items[0])).toBe(true)
expect(service.isSelected(items[1])).toBe(false)
})
})
it('should add item to selection', () => {
const { service, items } = createTestService()
using(service, () => {
service.addToSelection(items[0])
expect(service.selection.getValue()).toContain(items[0])
})
})
it('should remove item from selection', () => {
const { service, items } = createTestService()
using(service, () => {
service.selection.setValue([items[0], items[1]])
service.removeFromSelection(items[0])
expect(service.selection.getValue()).not.toContain(items[0])
expect(service.selection.getValue()).toContain(items[1])
})
})
it('should toggle selection on', () => {
const { service, items } = createTestService()
using(service, () => {
service.toggleSelection(items[0])
expect(service.isSelected(items[0])).toBe(true)
})
})
it('should toggle selection off', () => {
const { service, items } = createTestService()
using(service, () => {
service.selection.setValue([items[0]])
service.toggleSelection(items[0])
expect(service.isSelected(items[0])).toBe(false)
})
})
})
describe('handleKeyDown', () => {
it('should not handle keyboard when not focused', () => {
const { service, items } = createTestService()
using(service, () => {
service.hasFocus.setValue(false)
service.focusedItem.setValue(items[0])
service.handleKeyDown(new KeyboardEvent('keydown', { key: 'ArrowDown' }))
expect(service.focusedItem.getValue()).toBe(items[0])
})
})
it('should not handle ArrowDown (delegated to spatial navigation)', () => {
const { service, items } = createTestService()
using(service, () => {
service.hasFocus.setValue(true)
service.focusedItem.setValue(items[0])
const ev = new KeyboardEvent('keydown', { key: 'ArrowDown', cancelable: true })
const preventSpy = vi.spyOn(ev, 'preventDefault')
service.handleKeyDown(ev)
expect(service.focusedItem.getValue()).toBe(items[0])
expect(preventSpy).not.toHaveBeenCalled()
})
})
it('should not handle ArrowUp (delegated to spatial navigation)', () => {
const { service, items } = createTestService()
using(service, () => {
service.hasFocus.setValue(true)
service.focusedItem.setValue(items[1])
const ev = new KeyboardEvent('keydown', { key: 'ArrowUp', cancelable: true })
const preventSpy = vi.spyOn(ev, 'preventDefault')
service.handleKeyDown(ev)
expect(service.focusedItem.getValue()).toBe(items[1])
expect(preventSpy).not.toHaveBeenCalled()
})
})
it('should handle Home to move focus to first item', () => {
const { service, items } = createTestService()
using(service, () => {
service.hasFocus.setValue(true)
service.focusedItem.setValue(items[2])
service.handleKeyDown(new KeyboardEvent('keydown', { key: 'Home' }))
expect(service.focusedItem.getValue()).toBe(items[0])
})
})
it('should handle End to move focus to last item', () => {
const { service, items } = createTestService()
using(service, () => {
service.hasFocus.setValue(true)
service.focusedItem.setValue(items[0])
service.handleKeyDown(new KeyboardEvent('keydown', { key: 'End' }))
expect(service.focusedItem.getValue()).toBe(items[2])
})
})
it('should handle Space to toggle selection of focused item', () => {
const { service, items } = createTestService()
using(service, () => {
service.hasFocus.setValue(true)
service.focusedItem.setValue(items[0])
service.handleKeyDown(new KeyboardEvent('keydown', { key: ' ' }))
expect(service.selection.getValue()).toContain(items[0])
service.handleKeyDown(new KeyboardEvent('keydown', { key: ' ' }))
expect(service.selection.getValue()).not.toContain(items[0])
})
})
it('should handle + to select all items', () => {
const { service, items } = createTestService()
using(service, () => {
service.hasFocus.setValue(true)
service.handleKeyDown(new KeyboardEvent('keydown', { key: '+' }))
expect(service.selection.getValue().length).toBe(3)
expect(service.selection.getValue()).toEqual(items)
})
})
it('should handle - to deselect all items', () => {
const { service, items } = createTestService()
using(service, () => {
service.hasFocus.setValue(true)
service.selection.setValue([...items])
service.handleKeyDown(new KeyboardEvent('keydown', { key: '-' }))
expect(service.selection.getValue().length).toBe(0)
})
})
it('should handle * to invert selection', () => {
const { service, items } = createTestService()
using(service, () => {
service.hasFocus.setValue(true)
service.selection.setValue([items[0]])
service.handleKeyDown(new KeyboardEvent('keydown', { key: '*' }))
const selection = service.selection.getValue()
expect(selection).not.toContain(items[0])
expect(selection).toContain(items[1])
expect(selection).toContain(items[2])
})
})
it('should handle Insert to toggle selection and move to next item', () => {
const { service, items } = createTestService()
using(service, () => {
service.hasFocus.setValue(true)
service.focusedItem.setValue(items[0])
service.handleKeyDown(new KeyboardEvent('keydown', { key: 'Insert' }))
expect(service.selection.getValue()).toContain(items[0])
expect(service.focusedItem.getValue()).toBe(items[1])
})
})
it('should handle Insert to deselect already selected item', () => {
const { service, items } = createTestService()
using(service, () => {
service.hasFocus.setValue(true)
service.focusedItem.setValue(items[0])
service.selection.setValue([items[0]])
service.handleKeyDown(new KeyboardEvent('keydown', { key: 'Insert' }))
expect(service.selection.getValue()).not.toContain(items[0])
expect(service.focusedItem.getValue()).toBe(items[1])
})
})
it('should handle Escape to clear selection and search term', () => {
const { service, items } = createTestService()
using(service, () => {
service.hasFocus.setValue(true)
service.selection.setValue([items[0], items[1]])
service.searchTerm.setValue('test')
service.handleKeyDown(new KeyboardEvent('keydown', { key: 'Escape' }))
expect(service.selection.getValue()).toEqual([])
expect(service.searchTerm.getValue()).toBe('')
})
})
it('should handle type-ahead search when searchField is set', () => {
const { service, items } = createTestService({ searchField: 'name' })
using(service, () => {
service.hasFocus.setValue(true)
service.handleKeyDown(new KeyboardEvent('keydown', { key: 'S' }))
expect(service.searchTerm.getValue()).toBe('S')
expect(service.focusedItem.getValue()).toBe(items[1])
})
})
it('should accumulate type-ahead search characters', () => {
const { service, items } = createTestService({ searchField: 'name' })
using(service, () => {
service.hasFocus.setValue(true)
service.handleKeyDown(new KeyboardEvent('keydown', { key: 'T' }))
service.handleKeyDown(new KeyboardEvent('keydown', { key: 'h' }))
service.handleKeyDown(new KeyboardEvent('keydown', { key: 'i' }))
expect(service.searchTerm.getValue()).toBe('Thi')
expect(service.focusedItem.getValue()).toBe(items[2])
})
})
})
describe('handleItemClick', () => {
it('should set focused item on click', () => {
const { service, items } = createTestService()
using(service, () => {
service.handleItemClick(items[1], new MouseEvent('click'))
expect(service.focusedItem.getValue()).toBe(items[1])
})
})
it('should add to selection on Ctrl+Click', () => {
const { service, items } = createTestService()
using(service, () => {
service.handleItemClick(items[0], new MouseEvent('click', { ctrlKey: true }))
expect(service.selection.getValue()).toContain(items[0])
})
})
it('should remove from selection on Ctrl+Click when already selected', () => {
const { service, items } = createTestService()
using(service, () => {
service.selection.setValue([items[0]])
service.handleItemClick(items[0], new MouseEvent('click', { ctrlKey: true }))
expect(service.selection.getValue()).not.toContain(items[0])
})
})
it('should select range on Shift+Click', () => {
const { service, items } = createTestService()
using(service, () => {
service.focusedItem.setValue(items[0])
service.handleItemClick(items[2], new MouseEvent('click', { shiftKey: true }))
const selection = service.selection.getValue()
expect(selection).toContain(items[0])
expect(selection).toContain(items[1])
expect(selection).toContain(items[2])
})
})
it('should select range backwards on Shift+Click', () => {
const { service, items } = createTestService()
using(service, () => {
service.focusedItem.setValue(items[2])
service.handleItemClick(items[0], new MouseEvent('click', { shiftKey: true }))
const selection = service.selection.getValue()
expect(selection).toContain(items[0])
expect(selection).toContain(items[1])
expect(selection).toContain(items[2])
})
})
})
describe('handleItemDoubleClick', () => {
it('should not throw on double-click', () => {
const { service, items } = createTestService()
using(service, () => {
expect(() => service.handleItemDoubleClick(items[0])).not.toThrow()
})
})
})
describe('dispose', () => {
it('should dispose all observables', () => {
const { service } = createTestService()
expect(() => service[Symbol.dispose]()).not.toThrow()
})
})
})