|
11 | 11 | */ |
12 | 12 | import { act, type ReactNode, useState } from 'react' |
13 | 13 | import { createRoot, type Root } from 'react-dom/client' |
14 | | -import { afterEach, describe, expect, it, vi } from 'vitest' |
| 14 | +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' |
15 | 15 | import { InsideModalContext } from '../modal/modal' |
16 | 16 | import { Combobox } from './combobox' |
17 | 17 |
|
@@ -270,3 +270,92 @@ describe('Combobox pagination', () => { |
270 | 270 | expect(document.body.textContent).toContain('Showing the first 10,000 options') |
271 | 271 | }) |
272 | 272 | }) |
| 273 | + |
| 274 | +describe('Combobox virtualized options', () => { |
| 275 | + const options = Array.from({ length: 250 }, (_, index) => ({ |
| 276 | + label: `Model ${index}`, |
| 277 | + value: `model-${index}`, |
| 278 | + })) |
| 279 | + |
| 280 | + beforeEach(() => { |
| 281 | + /** JSDOM has no layout; use a fixed viewport and row height without mocking the virtualizer. */ |
| 282 | + vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockImplementation(function ( |
| 283 | + this: HTMLElement |
| 284 | + ) { |
| 285 | + return this.hasAttribute('data-index') ? 34 : 192 |
| 286 | + }) |
| 287 | + vi.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(300) |
| 288 | + }) |
| 289 | + |
| 290 | + describe.each([false, true])('disablePortal=%s', (disablePortal) => { |
| 291 | + it.each([99, 100, 250])('renders %i options on first open and reopen', (count) => { |
| 292 | + const onChange = vi.fn() |
| 293 | + render( |
| 294 | + <Combobox |
| 295 | + options={options.slice(0, count)} |
| 296 | + disablePortal={disablePortal} |
| 297 | + onChange={onChange} |
| 298 | + /> |
| 299 | + ) |
| 300 | + |
| 301 | + click(trigger()) |
| 302 | + |
| 303 | + expect(trigger('[data-option-index="0"]').textContent).toBe('Model 0') |
| 304 | + const renderedCount = document.querySelectorAll('[role="option"]').length |
| 305 | + expect(renderedCount).toBeGreaterThan(0) |
| 306 | + if (count >= 100) expect(renderedCount).toBeLessThan(count) |
| 307 | + |
| 308 | + click(trigger()) |
| 309 | + expect(document.querySelector('[role="listbox"]')).toBeNull() |
| 310 | + click(trigger()) |
| 311 | + |
| 312 | + mouseDown(trigger('[data-option-index="1"]')) |
| 313 | + expect(onChange).toHaveBeenCalledWith('model-1') |
| 314 | + }) |
| 315 | + }) |
| 316 | + |
| 317 | + it('renders a selected editable model on focus and supports keyboard selection', () => { |
| 318 | + const onChange = vi.fn() |
| 319 | + render(<Combobox options={options} editable value='model-0' onChange={onChange} />) |
| 320 | + |
| 321 | + const input = trigger('input[role="combobox"]') |
| 322 | + act(() => input.focus()) |
| 323 | + |
| 324 | + expect(trigger('[data-option-index="0"]').textContent).toBe('Model 0') |
| 325 | + press(input, 'ArrowDown') |
| 326 | + press(input, 'Enter') |
| 327 | + |
| 328 | + expect(onChange).toHaveBeenCalledWith('model-0') |
| 329 | + }) |
| 330 | + |
| 331 | + it('renders and selects options after scrolling beyond the initial window', () => { |
| 332 | + const onChange = vi.fn() |
| 333 | + render(<Combobox options={options} onChange={onChange} />) |
| 334 | + click(trigger()) |
| 335 | + |
| 336 | + expect(document.querySelector('[data-option-index="249"]')).toBeNull() |
| 337 | + const scrollArea = trigger('[role="listbox"]').parentElement |
| 338 | + if (!scrollArea) throw new Error('Scroll area was not rendered') |
| 339 | + act(() => { |
| 340 | + scrollArea.scrollTop = options.length * 34 - 192 |
| 341 | + scrollArea.dispatchEvent(new Event('scroll')) |
| 342 | + }) |
| 343 | + |
| 344 | + mouseDown(trigger('[data-option-index="249"]')) |
| 345 | + expect(onChange).toHaveBeenCalledWith('model-249') |
| 346 | + }) |
| 347 | + |
| 348 | + it('restores virtualized options after filtering below the threshold', () => { |
| 349 | + render(<Combobox options={options} searchable />) |
| 350 | + click(trigger()) |
| 351 | + const search = trigger('input[placeholder="Search..."]') as HTMLInputElement |
| 352 | + |
| 353 | + type(search, 'Model 249') |
| 354 | + expect(document.querySelectorAll('[role="option"]')).toHaveLength(1) |
| 355 | + expect(trigger('[role="option"]').textContent).toBe('Model 249') |
| 356 | + |
| 357 | + type(search, '') |
| 358 | + expect(trigger('[data-option-index="0"]').textContent).toBe('Model 0') |
| 359 | + expect(document.querySelectorAll('[role="option"]').length).toBeLessThan(options.length) |
| 360 | + }) |
| 361 | +}) |
0 commit comments