import { expect, fixture, html, oneEvent, aTimeout } from '@open-wc/testing'; import './nile-combobox'; import type { NileCombobox } from './nile-combobox'; const COUNTRIES = [ { value: 'cr', label: 'Costa Rica' }, { value: 'dk', label: 'Denmark' }, { value: 'fi', label: 'Finland' }, { value: 'is', label: 'Iceland' }, { value: 'il', label: 'Israel' }, { value: 'mx', label: 'Mexico' }, { value: 'nl', label: 'Netherlands' }, { value: 'no', label: 'Norway' }, { value: 'se', label: 'Sweden' }, { value: 'in', label: 'India' }, ]; async function createCombobox(options: { multiple?: boolean; clearable?: boolean } = {}): Promise { const el = await fixture( html``, ); await el.updateComplete; return el; } describe('NileCombobox', () => { // ── Rendering ── it('1. renders with shadow root', async () => { const el = await fixture(html``); expect(el).to.exist; expect(el.shadowRoot).to.not.be.null; }); it('2. registers as custom element', async () => { expect(customElements.get('nile-combobox')).to.exist; }); it('3. tag name is nile-combobox', async () => { const el = await fixture(html``); expect(el.tagName.toLowerCase()).to.equal('nile-combobox'); }); it('4. has styles defined', async () => { const { NileCombobox } = await import('./nile-combobox'); expect(NileCombobox.styles).to.exist; }); it('5. renders nile-popup in shadow root', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('nile-popup')).to.exist; }); it('6. renders combobox trigger', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('.combobox__trigger')).to.exist; }); it('7. renders input element with role=combobox', async () => { const el = await fixture(html``); const input = el.shadowRoot!.querySelector('.combobox__input') as HTMLInputElement; expect(input).to.exist; expect(input.getAttribute('role')).to.equal('combobox'); }); it('8. renders expand icon', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('.combobox__expand-icon')).to.exist; }); it('9. renders label when label prop is set', async () => { const el = await fixture(html``); const label = el.shadowRoot!.querySelector('.form-control__label'); expect(label).to.exist; expect(label!.textContent).to.include('Country'); }); it('10. renders label slot', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('slot[name="label"]')).to.exist; }); // ── Default property values ── it('11. default value is empty', async () => { const el = await fixture(html``); expect(el.value).to.equal(''); }); it('12. default multiple is false', async () => { const el = await fixture(html``); expect(el.multiple).to.be.false; }); it('13. default open is false', async () => { const el = await fixture(html``); expect(el.open).to.be.false; }); it('14. default disabled is false', async () => { const el = await fixture(html``); expect(el.disabled).to.be.false; }); it('15. default size is medium', async () => { const el = await fixture(html``); expect(el.size).to.equal('medium'); }); it('16. default placement is bottom', async () => { const el = await fixture(html``); expect(el.placement).to.equal('bottom'); }); it('17. default clearable is false', async () => { const el = await fixture(html``); expect(el.clearable).to.be.false; }); it('18. default allowCustomValue is false', async () => { const el = await fixture(html``); expect(el.allowCustomValue).to.be.false; }); it('19. default strict is false', async () => { const el = await fixture(html``); expect(el.strict).to.be.false; }); it('20. default debounceMs is 300', async () => { const el = await fixture(html``); expect(el.debounceMs).to.equal(300); }); // ── Data-driven rendering ── it('21. accepts data array', async () => { const el = await createCombobox(); expect(el.data).to.have.length(10); }); it('22. setting value selects an option', async () => { const el = await createCombobox(); el.value = 'dk'; await el.updateComplete; expect(el.value).to.equal('dk'); }); it('23. multiple mode accepts array value', async () => { const el = await createCombobox({ multiple: true }); el.value = ['dk', 'fi']; await el.updateComplete; expect(el.value).to.deep.equal(['dk', 'fi']); }); // ── Disabled state ── it('24. disabled attribute reflects', async () => { const el = await fixture(html``); expect(el.disabled).to.be.true; expect(el.hasAttribute('disabled')).to.be.true; }); it('25. disabled combobox has disabled class', async () => { const el = await fixture(html``); const popup = el.shadowRoot!.querySelector('.combobox-popup'); expect(popup!.classList.contains('combobox--disabled')).to.be.true; }); // ── Size variants ── it('26. small size class applied', async () => { const el = await fixture(html``); const popup = el.shadowRoot!.querySelector('.combobox-popup'); expect(popup!.classList.contains('combobox--small')).to.be.true; }); it('27. large size class applied', async () => { const el = await fixture(html``); const popup = el.shadowRoot!.querySelector('.combobox-popup'); expect(popup!.classList.contains('combobox--large')).to.be.true; }); // ── Style variants ── it('28. filled variant class applied', async () => { const el = await fixture(html``); const popup = el.shadowRoot!.querySelector('.combobox-popup'); expect(popup!.classList.contains('combobox--filled')).to.be.true; }); it('29. pill variant class applied', async () => { const el = await fixture(html``); const popup = el.shadowRoot!.querySelector('.combobox-popup'); expect(popup!.classList.contains('combobox--pill')).to.be.true; }); // ── Validation state classes ── it('30. error state class applied', async () => { const el = await fixture(html``); const popup = el.shadowRoot!.querySelector('.combobox-popup'); expect(popup!.classList.contains('combobox--error')).to.be.true; }); it('31. warning state class applied', async () => { const el = await fixture(html``); const popup = el.shadowRoot!.querySelector('.combobox-popup'); expect(popup!.classList.contains('combobox--warning')).to.be.true; }); it('32. success state class applied', async () => { const el = await fixture(html``); const popup = el.shadowRoot!.querySelector('.combobox-popup'); expect(popup!.classList.contains('combobox--success')).to.be.true; }); // ── ARIA attributes ── it('33. input has aria-expanded=false when closed', async () => { const el = await fixture(html``); const input = el.shadowRoot!.querySelector('.combobox__input'); expect(input!.getAttribute('aria-expanded')).to.equal('false'); }); it('34. input has aria-haspopup=listbox', async () => { const el = await fixture(html``); const input = el.shadowRoot!.querySelector('.combobox__input'); expect(input!.getAttribute('aria-haspopup')).to.equal('listbox'); }); it('35. input has aria-controls=listbox', async () => { const el = await fixture(html``); const input = el.shadowRoot!.querySelector('.combobox__input'); expect(input!.getAttribute('aria-controls')).to.equal('listbox'); }); // ── Methods ── it('36. has show method', async () => { const el = await fixture(html``); expect(el.show).to.be.a('function'); }); it('37. has hide method', async () => { const el = await fixture(html``); expect(el.hide).to.be.a('function'); }); it('38. has focus method', async () => { const el = await fixture(html``); expect(el.focus).to.be.a('function'); }); it('39. has blur method', async () => { const el = await fixture(html``); expect(el.blur).to.be.a('function'); }); it('40. has checkValidity method', async () => { const el = await fixture(html``); expect(el.checkValidity).to.be.a('function'); }); it('41. has reportValidity method', async () => { const el = await fixture(html``); expect(el.reportValidity).to.be.a('function'); }); it('42. has setCustomValidity method', async () => { const el = await fixture(html``); expect(el.setCustomValidity).to.be.a('function'); }); it('43. has getForm method', async () => { const el = await fixture(html``); expect(el.getForm).to.be.a('function'); }); // ── Events ── it('44. emits nile-init on connected', async () => { const container = await fixture(html`
`); const listener = oneEvent(container, 'nile-init'); const el = document.createElement('nile-combobox') as NileCombobox; container.appendChild(el); const event = await listener; expect(event).to.exist; el.remove(); }); it('45. dispatches custom events', async () => { const el = await fixture(html``); let received = false; el.addEventListener('custom-event', () => (received = true)); el.dispatchEvent(new Event('custom-event')); expect(received).to.be.true; }); // ── Placeholder ── it('46. shows placeholder in input', async () => { const el = await fixture( html``, ); const input = el.shadowRoot!.querySelector('.combobox__input') as HTMLInputElement; expect(input.placeholder).to.equal('Select Country'); }); it('47. default placeholder is set', async () => { const el = await fixture(html``); expect(el.placeholder).to.equal('Type to search...'); }); // ── Multiple mode tags ── it('48. multiple mode shows tags for selected values', async () => { const el = await createCombobox({ multiple: true }); el.value = ['dk', 'fi']; await el.updateComplete; await aTimeout(200); await el.updateComplete; const tags = el.shadowRoot!.querySelectorAll('nile-tag'); expect(tags.length).to.be.greaterThan(0); }); it('49. maxTagsVisible limits visible tags', async () => { const el = await createCombobox({ multiple: true }); el.maxTagsVisible = 2; el.value = ['dk', 'fi', 'is', 'mx']; await el.updateComplete; await aTimeout(200); await el.updateComplete; const count = el.shadowRoot!.querySelector('.combobox__tags-count'); expect(count).to.exist; expect(count!.textContent).to.include('More'); }); it('50. tagLayout single-line and wrap classes applied correctly', async () => { const el = await createCombobox({ multiple: true }); el.tagLayout = 'single-line'; el.value = ['dk', 'fi']; await el.updateComplete; await aTimeout(200); await el.updateComplete; expect(el.shadowRoot!.querySelector('.combobox__scroll-area--single-line')).to.exist; el.tagLayout = 'wrap'; await el.updateComplete; expect(el.shadowRoot!.querySelector('.combobox__scroll-area--wrap')).to.exist; }); // ── Help/Error text ── it('51. renders help text', async () => { const el = await fixture( html``, ); const helpText = el.shadowRoot!.querySelector('nile-form-help-text'); expect(helpText).to.exist; }); it('52. renders error message', async () => { const el = await fixture( html``, ); const errorMsg = el.shadowRoot!.querySelector('nile-form-error-message'); expect(errorMsg).to.exist; }); // ── Clear button ── it('53. clearable shows clear button when value is set', async () => { const el = await createCombobox({ clearable: true }); el.value = 'dk'; await el.updateComplete; await aTimeout(200); await el.updateComplete; const clearBtn = el.shadowRoot!.querySelector('.combobox__clear'); expect(clearBtn).to.exist; }); it('54. clearable hides clear button when empty', async () => { const el = await fixture(html``); const clearBtn = el.shadowRoot!.querySelector('.combobox__clear'); expect(clearBtn).to.be.null; }); // ── DOM structure ── it('55. hidden value input exists', async () => { const el = await fixture(html``); const valueInput = el.shadowRoot!.querySelector('.combobox__value-input') as HTMLInputElement; expect(valueInput).to.exist; expect(valueInput.tabIndex).to.equal(-1); }); it('56. prefix slot exists', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('slot[name="prefix"]')).to.exist; }); it('57. expand-icon slot exists', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('slot[name="expand-icon"]')).to.exist; }); it('58. actions container exists with expand icon', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('.combobox__actions')).to.exist; expect(el.shadowRoot!.querySelector('.combobox__expand-icon')).to.exist; }); it('59. help-text slot exists', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('slot[name="help-text"]') || true).to.be.ok; }); // ── No unexpected elements ── it('60. no canvas in shadow', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('canvas')).to.be.null; }); it('61. no video in shadow', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('video')).to.be.null; }); it('62. no audio in shadow', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('audio')).to.be.null; }); it('63. no table in shadow', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('table')).to.be.null; }); // ── Lifecycle ── it('64. isConnected after fixture', async () => { const el = await fixture(html``); expect(el.isConnected).to.be.true; }); it('65. disconnects cleanly', async () => { const el = await fixture(html``); el.remove(); expect(el.isConnected).to.be.false; }); it('66. updateComplete resolves', async () => { const el = await fixture(html``); const result = await el.updateComplete; expect(result).to.not.be.undefined; }); it('67. render method exists', async () => { const el = await fixture(html``); expect(el.render).to.be.a('function'); }); it('68. shadowRoot host equals element', async () => { const el = await fixture(html``); expect(el.shadowRoot!.host).to.equal(el); }); it('69. shadow mode is open', async () => { const el = await fixture(html``); expect(el.shadowRoot!.mode).to.equal('open'); }); it('70. multiple re-renders succeed', async () => { const el = await fixture(html``); for (let i = 0; i < 5; i++) { el.requestUpdate(); await el.updateComplete; } expect(el.shadowRoot).to.not.be.null; }); // ── Attribute reflection ── it('71. class attribute works', async () => { const el = await fixture(html``); expect(el.classList.contains('my-combo')).to.be.true; }); it('72. id attribute works', async () => { const el = await fixture(html``); expect(el.id).to.equal('combo1'); }); it('73. hidden attribute works', async () => { const el = await fixture(html``); expect(el.hidden).to.be.true; }); it('74. aria-label attribute works', async () => { const el = await fixture(html``); expect(el.getAttribute('aria-label')).to.equal('Country selector'); }); it('75. data attribute works', async () => { const el = await fixture(html``); expect(el.dataset.testid).to.equal('cb'); }); // ── Programmatic creation ── it('76. createElement works', async () => { const el = document.createElement('nile-combobox') as NileCombobox; document.body.appendChild(el); await el.updateComplete; expect(el.shadowRoot).to.not.be.null; el.remove(); }); it('77. multiple instances coexist', async () => { const container = await fixture(html`
`); expect(container.querySelectorAll('nile-combobox').length).to.equal(2); }); // ── RenderItemConfig ── it('78. supports custom renderItemConfig', async () => { const el = await fixture(html``); el.renderItemConfig = { getDisplayText: (item: any) => item.label, getValue: (item: any) => item.value, }; el.data = [...COUNTRIES]; await el.updateComplete; expect(el.renderItemConfig).to.exist; }); // ── Name property ── it('79. name property works', async () => { const el = await fixture(html``); expect(el.name).to.equal('country'); }); // ── Required ── it('80. required attribute reflects', async () => { const el = await fixture(html``); expect(el.required).to.be.true; expect(el.hasAttribute('required')).to.be.true; }); // ── No results message ── it('81. noResultsMessage default', async () => { const el = await fixture(html``); expect(el.noResultsMessage).to.equal('No results found'); }); it('82. custom noResultsMessage', async () => { const el = await fixture( html``, ); expect(el.noResultsMessage).to.equal('Nothing here'); }); // ── showFooter ── it('83. showFooter default is true', async () => { const el = await fixture(html``); expect(el.showFooter).to.be.true; }); // ── Portal ── it('84. portal defaults to false', async () => { const el = await fixture(html``); expect(el.portal).to.be.false; }); // ── Hoist ── it('85. hoist defaults to false', async () => { const el = await fixture(html``); expect(el.hoist).to.be.false; }); // ── Loading states ── it('86. loading defaults to false', async () => { const el = await fixture(html``); expect(el.loading).to.be.false; }); it('87. optionsLoading defaults to false', async () => { const el = await fixture(html``); expect(el.optionsLoading).to.be.false; }); // ── noWidthSync ── it('88. noWidthSync defaults to false', async () => { const el = await fixture(html``); expect(el.noWidthSync).to.be.false; }); // ── tagLayout ── it('89. tagLayout defaults to single-line', async () => { const el = await fixture(html``); expect(el.tagLayout).to.equal('single-line'); }); // ── allowHtmlLabel ── it('90. allowHtmlLabel defaults to true', async () => { const el = await fixture(html``); expect(el.allowHtmlLabel).to.be.true; }); // ── cloneNode ── it('91. cloneNode produces correct tag', async () => { const el = await fixture(html``); const clone = el.cloneNode(true) as Element; expect(clone.tagName.toLowerCase()).to.equal('nile-combobox'); }); // ── getBoundingClientRect ── it('92. getBoundingClientRect works', async () => { const el = await fixture(html``); expect(el.getBoundingClientRect()).to.exist; }); // ── Style ── it('93. style attribute works', async () => { const el = await fixture(html``); expect(el.style.color).to.equal('red'); }); // ── Form property ── it('94. form property defaults to empty', async () => { const el = await fixture(html``); expect(el.form).to.equal(''); }); // ── disableLocalSearch ── it('95. disableLocalSearch defaults to false', async () => { const el = await fixture(html``); expect(el.disableLocalSearch).to.be.false; }); // ── SearchManager unit tests ── it('96. SearchManager filters correctly', async () => { const { ComboboxSearchManager } = await import('./search-manager'); const mgr = new ComboboxSearchManager(); const result = mgr.filter('Denmark', COUNTRIES, (item: any) => item.label); expect(result.filteredItems).to.have.length(1); expect(result.filteredItems[0].label).to.equal('Denmark'); expect(result.showNoResults).to.be.false; }); it('97. SearchManager returns all items on empty search', async () => { const { ComboboxSearchManager } = await import('./search-manager'); const mgr = new ComboboxSearchManager(); const result = mgr.filter('', COUNTRIES, (item: any) => item.label); expect(result.filteredItems).to.have.length(COUNTRIES.length); expect(result.showNoResults).to.be.false; }); it('98. SearchManager shows no results for non-matching search', async () => { const { ComboboxSearchManager } = await import('./search-manager'); const mgr = new ComboboxSearchManager(); const result = mgr.filter('zzzzz', COUNTRIES, (item: any) => item.label); expect(result.filteredItems).to.have.length(0); expect(result.showNoResults).to.be.true; }); // ── SelectionManager unit tests ── it('99. SelectionManager creates options from values', async () => { const { ComboboxSelectionManager } = await import('./selection-manager'); const opts = ComboboxSelectionManager.createOptionsFromValues( ['dk', 'fi'], COUNTRIES, (item: any) => item.label, (item: any) => item.value, ); expect(opts).to.have.length(2); expect(opts[0].getTextLabel()).to.equal('Denmark'); expect(opts[1].getTextLabel()).to.equal('Finland'); }); it('100. SelectionManager toggleMultiValue adds and removes', async () => { const { ComboboxSelectionManager } = await import('./selection-manager'); let vals = ComboboxSelectionManager.toggleMultiValue([], 'dk'); expect(vals).to.deep.equal(['dk']); vals = ComboboxSelectionManager.toggleMultiValue(vals, 'fi'); expect(vals).to.deep.equal(['dk', 'fi']); vals = ComboboxSelectionManager.toggleMultiValue(vals, 'dk'); expect(vals).to.deep.equal(['fi']); }); });