Skip to content

Commit a1cd4d3

Browse files
committed
tests
1 parent b67db1c commit a1cd4d3

1 file changed

Lines changed: 199 additions & 0 deletions

File tree

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import { beforeEach, describe, expect, it, jest } from '@jest/globals'
2+
import { Combobox } from './Combobox'
3+
import './index'
4+
5+
function getPortalRoot() {
6+
const portalHost = document.querySelector('[data-solid-ui-combobox-portal]') as HTMLDivElement | null
7+
return portalHost?.shadowRoot ?? null
8+
}
9+
10+
async function flushUpdates() {
11+
await Promise.resolve()
12+
await Promise.resolve()
13+
}
14+
15+
describe('SolidUICombobox', () => {
16+
beforeEach(() => {
17+
document.body.innerHTML = ''
18+
})
19+
20+
it('is defined as a custom element', () => {
21+
expect(customElements.get('solid-ui-combobox')).toBe(Combobox)
22+
})
23+
24+
it('renders the input with label and placeholder', async () => {
25+
const combobox = new Combobox()
26+
combobox.label = 'Person'
27+
combobox.placeholder = 'Search people'
28+
29+
document.body.appendChild(combobox)
30+
await combobox.updateComplete
31+
32+
const label = combobox.shadowRoot?.querySelector('label.text-label') as HTMLLabelElement
33+
const input = combobox.shadowRoot?.querySelector('input.text-input') as HTMLInputElement
34+
const toggle = combobox.shadowRoot?.querySelector('button.dropdown-toggle') as HTMLButtonElement
35+
36+
expect(label).not.toBeNull()
37+
expect(label.textContent).toContain('Person')
38+
expect(input).not.toBeNull()
39+
expect(input.placeholder).toBe('Search people')
40+
expect(input.getAttribute('role')).toBe('combobox')
41+
expect(input.getAttribute('aria-expanded')).toBe('false')
42+
expect(toggle).not.toBeNull()
43+
})
44+
45+
it('loads suggestions from suggestionProvider and emits input events', async () => {
46+
const combobox = new Combobox()
47+
const inputEvents = jest.fn()
48+
const suggestionProvider = jest.fn(async (query: string) => [
49+
{ label: `Alice ${query}`, value: 'alice' },
50+
{ label: `Bob ${query}`, value: 'bob' }
51+
])
52+
53+
combobox.suggestionProvider = suggestionProvider
54+
combobox.addEventListener('input', (event: Event) => {
55+
inputEvents((event as CustomEvent).detail)
56+
})
57+
58+
document.body.appendChild(combobox)
59+
await combobox.updateComplete
60+
61+
const input = combobox.shadowRoot?.querySelector('input.text-input') as HTMLInputElement
62+
input.value = 'al'
63+
input.dispatchEvent(new Event('input', { bubbles: true, composed: true }))
64+
65+
await flushUpdates()
66+
await combobox.updateComplete
67+
68+
const portalRoot = getPortalRoot()
69+
const options = Array.from(portalRoot?.querySelectorAll('[role="option"]') as NodeListOf<HTMLElement>)
70+
71+
expect(suggestionProvider).toHaveBeenCalledWith('al')
72+
expect(inputEvents).toHaveBeenCalledWith({ value: 'al' })
73+
expect(combobox.inputValue).toBe('al')
74+
expect(options).toHaveLength(2)
75+
expect(options[0].textContent).toContain('Alice al')
76+
})
77+
78+
it('renders the selected option first in the popup', async () => {
79+
const combobox = new Combobox()
80+
combobox.options = [
81+
{ label: 'English', value: 'en' },
82+
{ label: 'French', value: 'fr' },
83+
{ label: 'Spanish', value: 'es' }
84+
]
85+
combobox.value = 'fr'
86+
87+
document.body.appendChild(combobox)
88+
await combobox.updateComplete
89+
90+
const input = combobox.shadowRoot?.querySelector('input.text-input') as HTMLInputElement
91+
input.dispatchEvent(new Event('focus'))
92+
await combobox.updateComplete
93+
94+
const portalRoot = getPortalRoot()
95+
const options = Array.from(portalRoot?.querySelectorAll('[role="option"]') as NodeListOf<HTMLElement>)
96+
97+
expect(options).toHaveLength(3)
98+
expect(options[0].textContent).toContain('French')
99+
expect(options[0].getAttribute('aria-selected')).toBe('true')
100+
})
101+
102+
it('updates value and emits change when an option is clicked', async () => {
103+
const combobox = new Combobox()
104+
const changed = jest.fn()
105+
106+
combobox.options = [
107+
{ label: 'Alice', value: 'alice', publicId: 'https://example.com/alice' },
108+
{ label: 'Bob', value: 'bob' }
109+
]
110+
111+
combobox.addEventListener('change', (event: Event) => {
112+
changed((event as CustomEvent).detail)
113+
})
114+
115+
document.body.appendChild(combobox)
116+
await combobox.updateComplete
117+
118+
const input = combobox.shadowRoot?.querySelector('input.text-input') as HTMLInputElement
119+
input.dispatchEvent(new Event('focus'))
120+
await combobox.updateComplete
121+
122+
const portalRoot = getPortalRoot()
123+
const options = portalRoot?.querySelectorAll('[role="option"]') as NodeListOf<HTMLElement>
124+
options[1].click()
125+
await combobox.updateComplete
126+
127+
expect(combobox.value).toBe('bob')
128+
expect(combobox.inputValue).toBe('Bob')
129+
expect(input.getAttribute('aria-expanded')).toBe('false')
130+
expect(changed).toHaveBeenCalledWith({
131+
value: 'bob',
132+
label: 'Bob',
133+
option: { label: 'Bob', value: 'bob' }
134+
})
135+
})
136+
137+
it('supports keyboard selection from the input', async () => {
138+
const combobox = new Combobox()
139+
const changed = jest.fn()
140+
141+
combobox.options = [
142+
{ label: 'Alice', value: 'alice' },
143+
{ label: 'Bob', value: 'bob' },
144+
{ label: 'Carol', value: 'carol' }
145+
]
146+
147+
combobox.addEventListener('change', (event: Event) => {
148+
changed((event as CustomEvent).detail)
149+
})
150+
151+
document.body.appendChild(combobox)
152+
await combobox.updateComplete
153+
154+
const input = combobox.shadowRoot?.querySelector('input.text-input') as HTMLInputElement
155+
156+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))
157+
await combobox.updateComplete
158+
159+
expect(input.getAttribute('aria-expanded')).toBe('true')
160+
expect(input.getAttribute('aria-activedescendant')).toBeTruthy()
161+
162+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))
163+
await combobox.updateComplete
164+
165+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
166+
await combobox.updateComplete
167+
168+
expect(combobox.value).toBe('bob')
169+
expect(combobox.inputValue).toBe('Bob')
170+
expect(changed).toHaveBeenCalledWith({
171+
value: 'bob',
172+
label: 'Bob',
173+
option: { label: 'Bob', value: 'bob' }
174+
})
175+
})
176+
177+
it('closes the popup when clicking outside the component', async () => {
178+
const combobox = new Combobox()
179+
combobox.options = [
180+
{ label: 'Alice', value: 'alice' },
181+
{ label: 'Bob', value: 'bob' }
182+
]
183+
184+
document.body.appendChild(combobox)
185+
await combobox.updateComplete
186+
187+
const input = combobox.shadowRoot?.querySelector('input.text-input') as HTMLInputElement
188+
input.dispatchEvent(new Event('focus'))
189+
await combobox.updateComplete
190+
191+
expect(input.getAttribute('aria-expanded')).toBe('true')
192+
expect(getPortalRoot()).not.toBeNull()
193+
194+
document.body.dispatchEvent(new Event('pointerdown', { bubbles: true, composed: true }))
195+
await combobox.updateComplete
196+
197+
expect(input.getAttribute('aria-expanded')).toBe('false')
198+
})
199+
})

0 commit comments

Comments
 (0)