/** Group D (selection overlays) component tests. Panels portal to
. */
import { describe, expect, it, afterEach } from 'vitest'
import { mount, unmount, flushSync } from 'svelte'
import SvListBox from './SvListBox.svelte'
import SvDropDownList from './SvDropDownList.svelte'
import SvComboBox from './SvComboBox.svelte'
import SvTagsInput from './SvTagsInput.svelte'
import SvCountryInput from './SvCountryInput.svelte'
function mnt(Comp: any, props: Record) {
const target = document.createElement('div')
document.body.appendChild(target)
const app = mount(Comp, { target, props: props as any })
flushSync()
return { target, destroy: () => { unmount(app); target.remove() } }
}
afterEach(() => {
document.querySelectorAll('.sv-ddl__panel, .sv-country__panel').forEach((n) => n.remove())
})
const OPTS = [
{ value: 'a', label: 'Apple' },
{ value: 'b', label: 'Banana' },
{ value: 'c', label: 'Cherry' },
]
describe('SvListBox', () => {
it('single select emits scalar; multi toggles array', () => {
let got: any
const a = mnt(SvListBox, { options: OPTS, value: 'a', onChange: (v: any) => (got = v) })
try {
a.target.querySelectorAll('.sv-listbox__opt')[2]!.click(); flushSync()
expect(got).toBe('c')
} finally { a.destroy() }
const b = mnt(SvListBox, { options: OPTS, value: ['a'], multiple: true, onChange: (v: any) => (got = v) })
try {
b.target.querySelectorAll('.sv-listbox__opt')[1]!.click(); flushSync()
expect(got).toEqual(['a', 'b'])
} finally { b.destroy() }
})
it('posts its value(s) via hidden inputs when `name` is set (multi -> repeated name)', () => {
const m = mnt(SvListBox, { options: OPTS, value: ['a', 'c'], multiple: true, name: 'tags' })
try {
const hidden = Array.from(m.target.querySelectorAll('input[type="hidden"][name="tags"]'))
expect(hidden.map((h) => h.value)).toEqual(['a', 'c'])
} finally { m.destroy() }
const s = mnt(SvListBox, { options: OPTS, value: 'b', name: 'choice' })
try {
const hidden = Array.from(s.target.querySelectorAll('input[type="hidden"][name="choice"]'))
expect(hidden.map((h) => h.value)).toEqual(['b'])
} finally { s.destroy() }
})
})
describe('SvDropDownList', () => {
it('opens a portalled listbox and picks a value', () => {
let got: any
const { target, destroy } = mnt(SvDropDownList, { options: OPTS, value: null, onChange: (v: any) => (got = v) })
try {
target.querySelector('.sv-ddl')!.click(); flushSync()
const panel = document.querySelector('.sv-ddl__panel')!
expect(panel).not.toBeNull()
panel.querySelectorAll('.sv-ddl__opt')[1]!.click(); flushSync()
expect(got).toBe('b')
expect(document.querySelector('.sv-ddl__panel')).toBeNull() // closed
} finally { destroy() }
})
it('readonly does not open the panel and marks aria-readonly', () => {
let got: any
const { target, destroy } = mnt(SvDropDownList, { options: OPTS, value: 'a', readonly: true, onChange: (v: any) => (got = v) })
try {
const trigger = target.querySelector('.sv-ddl')!
expect(trigger.getAttribute('aria-readonly')).toBe('true')
expect(trigger.hasAttribute('disabled')).toBe(false) // still focusable
trigger.click(); flushSync()
expect(document.querySelector('.sv-ddl__panel')).toBeNull() // stayed closed
expect(got).toBeUndefined()
} finally { destroy() }
})
it('renders a resize grip only when resizable and the panel opens downward', () => {
const off = mnt(SvDropDownList, { options: OPTS, value: null })
try {
off.target.querySelector('.sv-ddl')!.click(); flushSync()
expect(document.querySelector('.sv-ddl__grip')).toBeNull() // no grip by default
} finally { off.destroy() }
const on = mnt(SvDropDownList, { options: OPTS, value: null, resizable: true })
try {
on.target.querySelector('.sv-ddl')!.click(); flushSync()
expect(document.querySelector('.sv-ddl__panel.is-resizable')).not.toBeNull()
expect(document.querySelector('.sv-ddl__grip')).not.toBeNull()
} finally { on.destroy() }
})
})
describe('SvComboBox', () => {
it('filters as you type and picks a match', () => {
let got: any
const { target, destroy } = mnt(SvComboBox, { options: OPTS, value: null, onChange: (v: any) => (got = v) })
try {
const input = target.querySelector('.sv-combo__input')!
input.dispatchEvent(new FocusEvent('focus')); flushSync()
input.value = 'ban'
input.dispatchEvent(new Event('input', { bubbles: true })); flushSync()
const opts = document.querySelectorAll('.sv-ddl__opt')
expect(opts.length).toBe(1)
opts[0]!.click(); flushSync()
expect(got).toBe('b')
} finally { destroy() }
})
it('shows a resize grip on the open panel when resizable', () => {
const { target, destroy } = mnt(SvComboBox, { options: OPTS, value: null, resizable: true })
try {
const input = target.querySelector('.sv-combo__input')!
input.dispatchEvent(new FocusEvent('focus')); flushSync()
input.value = 'b'
input.dispatchEvent(new Event('input', { bubbles: true })); flushSync()
expect(document.querySelector('.sv-ddl__panel.is-resizable')).not.toBeNull()
expect(document.querySelector('.sv-ddl__grip')).not.toBeNull()
} finally { destroy() }
})
})
describe('SvTagsInput', () => {
it('adds on Enter and removes on Backspace', () => {
let tags: string[] = ['x']
const { target, destroy } = mnt(SvTagsInput, { value: tags, onChange: (t: string[]) => (tags = t) })
try {
const input = target.querySelector('.sv-tags__input')!
input.value = 'new'
// Svelte bind:value needs an input event before the keydown reads draft.
input.dispatchEvent(new Event('input', { bubbles: true })); flushSync()
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })); flushSync()
expect(tags).toEqual(['x', 'new'])
} finally { destroy() }
})
})
describe('SvCountryInput', () => {
it('searches and selects a country code', () => {
let got: any
const { target, destroy } = mnt(SvCountryInput, { value: null, onChange: (v: any) => (got = v) })
try {
target.querySelector('.sv-country')!.click(); flushSync()
const search = document.querySelector('.sv-country__search')!
search.value = 'germ'
search.dispatchEvent(new Event('input', { bubbles: true })); flushSync()
const opts = document.querySelectorAll('.sv-country__opt')
expect(opts.length).toBe(1)
opts[0]!.click(); flushSync()
expect(got).toBe('DE')
} finally { destroy() }
})
})