import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing'; import './nile-command-menu'; import '../nile-command-menu-group/nile-command-menu-group'; import '../nile-command-menu-item/nile-command-menu-item'; import type { NileCommandMenu } from './nile-command-menu'; import type { NileCommandMenuItem } from '../nile-command-menu-item/nile-command-menu-item'; const withItems = html` Home Settings New file Disabled `; describe('NileCommandMenu', () => { it('1. renders', async () => { const el = await fixture(html``); expect(el).to.exist; }); it('2. shadow root', async () => { const el = await fixture(html``); expect(el.shadowRoot).to.not.be.null; }); it('3. tag name', async () => { const el = await fixture(html``); expect(el.tagName.toLowerCase()).to.equal('nile-command-menu'); }); it('4. open defaults false', async () => { const el = await fixture(html``); expect(el.open).to.be.false; }); it('5. placeholder default', async () => { const el = await fixture(html``); expect(el.placeholder).to.equal('Search commands...'); }); it('6. label default', async () => { const el = await fixture(html``); expect(el.label).to.equal('Command menu'); }); it('7. noResultsMessage default', async () => { const el = await fixture(html``); expect(el.noResultsMessage).to.equal('No results found'); }); it('8. customSearch defaults false', async () => { const el = await fixture(html``); expect(el.customSearch).to.be.false; }); it('9. keepOpenOnSelect defaults false', async () => { const el = await fixture(html``); expect(el.keepOpenOnSelect).to.be.false; }); it('10. preventOverlayClose defaults false', async () => { const el = await fixture(html``); expect(el.preventOverlayClose).to.be.false; }); it('11. base part', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('[part="base"]')).to.exist; }); it('12. overlay part', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('[part="overlay"]')).to.exist; }); it('13. panel part', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('[part="panel"]')).to.exist; }); it('14. input part', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('[part="input"]')).to.exist; }); it('15. list part', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('[part="list"]')).to.exist; }); it('16. footer part', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('[part="footer"]')).to.exist; }); it('17. empty part', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('[part="empty"]')).to.exist; }); it('18. default slot', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('slot:not([name])')).to.exist; }); it('19. footer slot', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('slot[name="footer"]')).to.exist; }); it('20. empty slot', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('slot[name="empty"]')).to.exist; }); it('21. dialog role', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('[role="dialog"]')).to.exist; }); it('22. listbox role', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('[role="listbox"]')).to.exist; }); it('23. input combobox role', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('input[role="combobox"]')).to.exist; }); it('24. placeholder applied', async () => { const el = await fixture(html``); await el.updateComplete; expect(el.shadowRoot!.querySelector('input')!.placeholder).to.equal('Find...'); }); it('25. show method', async () => { const el = await fixture(html``); expect(el.show).to.be.a('function'); }); it('26. hide method', async () => { const el = await fixture(html``); expect(el.hide).to.be.a('function'); }); it('27. show opens', async () => { const el = await fixture(withItems); await el.show(); expect(el.open).to.be.true; await el.hide(); }); it('28. nile-show event', async () => { const el = await fixture(withItems); setTimeout(() => el.show()); const ev = await oneEvent(el, 'nile-show'); expect(ev).to.exist; await el.hide(); }); it('29. nile-after-show event', async () => { const el = await fixture(withItems); setTimeout(() => el.show()); const ev = await oneEvent(el, 'nile-after-show'); expect(ev).to.exist; await el.hide(); }); it('30. hide closes', async () => { const el = await fixture(withItems); await el.show(); await el.hide(); expect(el.open).to.be.false; }); it('31. filters items by query', async () => { const el = await fixture(withItems); await el.show(); const input = el.shadowRoot!.querySelector('input')!; input.value = 'settings'; input.dispatchEvent(new Event('input')); await el.updateComplete; const visible = [...el.querySelectorAll('nile-command-menu-item')].filter(i => !i.hidden); expect(visible.length).to.equal(1); expect(visible[0].value).to.equal('settings'); await el.hide(); }); it('32. hides empty groups', async () => { const el = await fixture(withItems); await el.show(); const input = el.shadowRoot!.querySelector('input')!; input.value = 'home'; input.dispatchEvent(new Event('input')); await el.updateComplete; const groups = el.querySelectorAll('nile-command-menu-group'); expect(groups[0].hidden).to.be.false; // Pages (has Home) expect(groups[1].hidden).to.be.true; // Actions (no match) await el.hide(); }); it('33. shows empty state on no match', async () => { const el = await fixture(withItems); await el.show(); const input = el.shadowRoot!.querySelector('input')!; input.value = 'zzzznomatch'; input.dispatchEvent(new Event('input')); await el.updateComplete; expect(el.shadowRoot!.querySelector('.command__empty--visible')).to.exist; await el.hide(); }); it('34. emits nile-search', async () => { const el = await fixture(withItems); await el.show(); const input = el.shadowRoot!.querySelector('input')!; setTimeout(() => { input.value = 'x'; input.dispatchEvent(new Event('input')); }); const ev = await oneEvent(el, 'nile-search'); expect(ev.detail.value).to.equal('x'); await el.hide(); }); it('35. emits nile-select on click', async () => { const el = await fixture(withItems); await el.show(); const item = el.querySelector('nile-command-menu-item[value="home"]')!; setTimeout(() => item.click()); const ev = await oneEvent(el, 'nile-select'); expect(ev.detail.value).to.equal('home'); }); it('36. select closes by default', async () => { const el = await fixture(withItems); await el.show(); const item = el.querySelector('nile-command-menu-item[value="home"]')!; item.click(); await waitUntil(() => !el.open); expect(el.open).to.be.false; }); it('37. keep-open-on-select stays open', async () => { const el = await fixture(html` Home `); await el.show(); el.querySelector('nile-command-menu-item')!.click(); await el.updateComplete; expect(el.open).to.be.true; await el.hide(); }); it('38. disabled item not selected on click', async () => { const el = await fixture(withItems); await el.show(); let fired = false; el.addEventListener('nile-select', () => (fired = true)); el.querySelector('nile-command-menu-item[value="disabled"]')!.click(); await el.updateComplete; expect(fired).to.be.false; await el.hide(); }); it('39. arrow down moves active item', async () => { const el = await fixture(withItems); await el.show(); const panel = el.shadowRoot!.querySelector('[role="dialog"]')!; panel.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })); await el.updateComplete; const active = [...el.querySelectorAll('nile-command-menu-item')].filter(i => i.active); expect(active.length).to.equal(1); await el.hide(); }); it('40. first visible item active on open', async () => { const el = await fixture(withItems); await el.show(); const active = [...el.querySelectorAll('nile-command-menu-item')].filter(i => i.active); expect(active.length).to.equal(1); expect(active[0].value).to.equal('home'); await el.hide(); }); it('41. enter selects active item', async () => { const el = await fixture(withItems); await el.show(); const panel = el.shadowRoot!.querySelector('[role="dialog"]')!; setTimeout(() => panel.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))); const ev = await oneEvent(el, 'nile-select'); expect(ev.detail.value).to.equal('home'); }); it('42. custom-search does not auto-filter', async () => { const el = await fixture(html` Alpha Beta `); await el.show(); const input = el.shadowRoot!.querySelector('input')!; input.value = 'zzz'; input.dispatchEvent(new Event('input')); await el.updateComplete; const visible = [...el.querySelectorAll('nile-command-menu-item')].filter(i => !i.hidden); expect(visible.length).to.equal(2); await el.hide(); }); it('43. nile-request-close cancelable', async () => { const el = await fixture(withItems); await el.show(); el.addEventListener('nile-request-close', e => e.preventDefault()); const overlay = el.shadowRoot!.querySelector('.command__overlay') as HTMLElement; overlay.click(); await el.updateComplete; expect(el.open).to.be.true; await el.hide(); }); it('44. overlay click closes', async () => { const el = await fixture(withItems); await el.show(); const overlay = el.shadowRoot!.querySelector('.command__overlay') as HTMLElement; overlay.click(); await waitUntil(() => !el.open); expect(el.open).to.be.false; }); it('45. has-footer class when footer slotted', async () => { const el = await fixture(html`
footer
`); await el.updateComplete; expect(el.shadowRoot!.querySelector('.command--has-footer')).to.exist; }); it('46. resets query on reopen', async () => { const el = await fixture(withItems); await el.show(); const input = el.shadowRoot!.querySelector('input')!; input.value = 'home'; input.dispatchEvent(new Event('input')); await el.updateComplete; await el.hide(); await el.show(); expect(el.value).to.equal(''); await el.hide(); }); it('S1. scopes empty by default', async () => { const el = await fixture(html``); expect(el.scopes).to.deep.equal([]); }); it('S2. no scopes row when empty', async () => { const el = await fixture(html``); await el.updateComplete; expect(el.shadowRoot!.querySelector('[part="scopes"]')).to.be.null; }); it('S3. scopes attribute parses comma list', async () => { const el = await fixture(html``); expect(el.scopes.map(s => s.value)).to.deep.equal(['pages', 'actions']); }); it('S4. scopes row renders tags', async () => { const el = await fixture(html``); await el.updateComplete; expect(el.shadowRoot!.querySelector('[part="scopes"]')).to.exist; expect(el.shadowRoot!.querySelectorAll('nile-tag').length).to.equal(2); }); it('S5. default scopes label', async () => { const el = await fixture(html``); expect(el.scopesLabel).to.equal('Searching for'); }); it('S6. scope filters items', async () => { const el = await fixture(html` Home Run `); await el.show(); el.scopes = [{ label: 'Pages', value: 'pages' }]; await el.updateComplete; const visible = [...el.querySelectorAll('nile-command-menu-item')].filter(i => !i.hidden); expect(visible.map(i => i.value)).to.deep.equal(['home']); await el.hide(); }); it('S7. scoped item with no scope always shown', async () => { const el = await fixture(html` Home Global Run `); await el.show(); const visible = [...el.querySelectorAll('nile-command-menu-item')].filter(i => !i.hidden); expect(visible.map(i => i.value)).to.have.members(['home', 'global']); expect(visible.map(i => i.value)).to.not.include('run'); await el.hide(); }); it('S8. removing a scope emits event and refilters', async () => { const el = await fixture(html` Home Run `); await el.show(); const tag = el.shadowRoot!.querySelector('nile-tag')!; // first tag = Pages setTimeout(() => tag.dispatchEvent(new CustomEvent('nile-remove', { bubbles: true, composed: true }))); const ev = await oneEvent(el, 'nile-scope-remove'); expect(ev.detail.scope.value).to.equal('pages'); await el.updateComplete; expect(el.scopes.map(s => s.value)).to.deep.equal(['actions']); const visible = [...el.querySelectorAll('nile-command-menu-item')].filter(i => !i.hidden); expect(visible.map(i => i.value)).to.deep.equal(['run']); await el.hide(); }); it('47a. footer hints render by default', async () => { const el = await fixture(html``); await el.updateComplete; expect(el.shadowRoot!.querySelector('[part="hints"]')).to.exist; expect(el.shadowRoot!.querySelectorAll('.command__footer kbd').length).to.be.greaterThan(0); }); it('47b. has-footer by default (hints)', async () => { const el = await fixture(html``); await el.updateComplete; expect(el.shadowRoot!.querySelector('.command--has-footer')).to.exist; }); it('47c. no-footer-hints removes hints', async () => { const el = await fixture(html``); await el.updateComplete; expect(el.noFooterHints).to.be.true; expect(el.shadowRoot!.querySelector('[part="hints"]')).to.be.null; }); it('47d. no-footer-hints hides footer', async () => { const el = await fixture(html``); await el.updateComplete; expect(el.shadowRoot!.querySelector('.command--has-footer')).to.be.null; }); it('47. is defined', async () => { expect(customElements.get('nile-command-menu')).to.exist; }); it('48. has styles', async () => { expect((await import('./nile-command-menu')).NileCommandMenu.styles).to.exist; }); it('49. open reflects', async () => { const el = await fixture(html``); expect(el.hasAttribute('open')).to.be.true; }); it('50. createElement', async () => { const el = document.createElement('nile-command-menu') as NileCommandMenu; document.body.appendChild(el); await el.updateComplete; expect(el.shadowRoot).to.not.be.null; document.body.removeChild(el); }); });