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); });
});