import { expect, fixture, html, aTimeout, defineCE, unsafeStatic, nextFrame, } from '@open-wc/testing'; import { LitElement } from '@lion/core'; import '@lion/option/lion-option.js'; import { OverlayController } from '@lion/overlays'; import './keyboardEventShimIE.js'; import '../lion-options.js'; import '../lion-select-rich.js'; import { LionSelectRich } from '../index.js'; describe('lion-select-rich', () => { it(`has a fieldName based on the label`, async () => { const el1 = await fixture( html` `, ); expect(el1.fieldName).to.equal(el1._labelNode.textContent); const el2 = await fixture( html` `, ); expect(el2.fieldName).to.equal(el2._labelNode.textContent); }); it(`has a fieldName based on the name if no label exists`, async () => { const el = await fixture( html` `, ); expect(el.fieldName).to.equal(el.name); }); it(`can override fieldName`, async () => { const el = await fixture( html` `, ); expect(el.__fieldName).to.equal(el.fieldName); }); it('does not have a tabindex', async () => { const el = await fixture(html` `); expect(el.hasAttribute('tabindex')).to.be.false; }); it('delegates the name attribute to its children options', async () => { const el = await fixture(html` Item 1 Item 2 `); const optOne = el.querySelectorAll('lion-option')[0]; const optTwo = el.querySelectorAll('lion-option')[1]; expect(optOne.name).to.equal('foo[]'); expect(optTwo.name).to.equal('foo[]'); }); describe('Invoker', () => { it('generates an lion-select-invoker if no invoker is provided', async () => { const el = await fixture(html` `); expect(el._invokerNode).to.exist; expect(el._invokerNode.tagName).to.equal('LION-SELECT-INVOKER'); }); it('syncs the selected element to the invoker', async () => { const el = await fixture(html` Item 1 Item 2 `); const options = Array.from(el.querySelectorAll('lion-option')); expect(el._invokerNode.selectedElement).to.equal(options[0]); el.checkedIndex = 1; expect(el._invokerNode.selectedElement).to.equal(el.querySelectorAll('lion-option')[1]); }); it('delegates readonly to the invoker', async () => { const el = await fixture(html` Item 1 Item 2 `); expect(el.hasAttribute('readonly')).to.be.true; expect(el._invokerNode.hasAttribute('readonly')).to.be.true; }); }); describe('overlay', () => { it('should be closed by default', async () => { const el = await fixture(html` `); expect(el.opened).to.be.false; }); it('shows/hides the listbox via opened attribute', async () => { const el = await fixture(html` `); el.opened = true; await el.updateComplete; expect(el._overlayCtrl.isShown).to.be.true; el.opened = false; await el.updateComplete; expect(el._overlayCtrl.isShown).to.be.false; }); it('syncs opened state with overlay shown', async () => { const el = await fixture(html` `); const outerEl = await fixture(''); expect(el.opened).to.be.true; // a click on the button will trigger hide on outside click // which we then need to sync back to "opened" outerEl.click(); await aTimeout(); expect(el.opened).to.be.false; }); it('will focus the listbox on open and invoker on close', async () => { const el = await fixture(html` `); await el._overlayCtrl.show(); await el.updateComplete; expect(document.activeElement === el._listboxNode).to.be.true; expect(document.activeElement === el._invokerNode).to.be.false; el.opened = false; await el.updateComplete; expect(document.activeElement === el._listboxNode).to.be.false; expect(document.activeElement === el._invokerNode).to.be.true; }); it('opens the listbox with checked option as active', async () => { const el = await fixture(html` Item 1 Item 2 `); await el._overlayCtrl.show(); await el.updateComplete; const options = Array.from(el.querySelectorAll('lion-option')); expect(options[1].active).to.be.true; expect(options[1].checked).to.be.true; }); it('stays closed on click if it disabled or readonly', async () => { const elReadOnly = await fixture(html` Item 1 Item 2 `); const elDisabled = await fixture(html` Item 1 Item 2 `); elReadOnly._invokerNode.click(); await elReadOnly.updateComplete; expect(elReadOnly.opened).to.be.false; elDisabled._invokerNode.click(); await elDisabled.updateComplete; expect(elDisabled.opened).to.be.false; }); }); describe('interaction-mode', () => { it('allows to specify an interaction-mode which determines other behaviors', async () => { const el = await fixture(html` `); expect(el.interactionMode).to.equal('mac'); }); }); describe('Keyboard navigation', () => { it('opens the listbox with [Enter] key via click handler', async () => { const el = await fixture(html` `); el._invokerNode.click(); await el.updateComplete; expect(el.opened).to.be.true; }); it('opens the listbox with [ ](Space) key via click handler', async () => { const el = await fixture(html` `); el._invokerNode.click(); await el.updateComplete; expect(el.opened).to.be.true; }); it('closes the listbox with [Escape] key once opened', async () => { const el = await fixture(html` `); el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' })); await el.updateComplete; expect(el.opened).to.be.false; }); it('closes the listbox with [Tab] key once opened', async () => { const el = await fixture(html` `); // tab can only be caught via keydown el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' })); await el.updateComplete; expect(el.opened).to.be.false; }); }); describe('Mouse navigation', () => { it('opens the listbox via click on invoker', async () => { const el = await fixture(html` `); expect(el.opened).to.be.false; el._invokerNode.click(); await nextFrame(); expect(el.opened).to.be.true; }); it('closes the listbox when an option gets clicked', async () => { const el = await fixture(html` Item 1 `); expect(el.opened).to.be.true; el.querySelector('lion-option').click(); expect(el.opened).to.be.false; }); }); describe('Keyboard navigation Windows', () => { it('closes the listbox with [Enter] key once opened', async () => { const el = await fixture(html` `); el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' })); await el.updateComplete; expect(el.opened).to.be.false; }); }); describe('Keyboard navigation Mac', () => { it('checks active item and closes the listbox with [Enter] key via click handler once opened', async () => { const el = await fixture(html` Item 1 Item 2 `); // changes active but not checked el.activeIndex = 1; expect(el.checkedIndex).to.equal(0); el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' })); expect(el.opened).to.be.false; expect(el.checkedIndex).to.equal(1); }); it('opens the listbox with [ArrowUp] key', async () => { const el = await fixture(html` `); el.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowUp' })); await el.updateComplete; expect(el.opened).to.be.true; }); it('opens the listbox with [ArrowDown] key', async () => { const el = await fixture(html` `); el.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' })); await el.updateComplete; expect(el.opened).to.be.true; }); }); describe('Accessibility', () => { it('has the right references to its inner elements', async () => { const el = await fixture(html` Item 1 Item 2 `); expect(el._invokerNode.getAttribute('aria-labelledby')).to.contain(el._labelNode.id); expect(el._invokerNode.getAttribute('aria-labelledby')).to.contain(el._invokerNode.id); expect(el._invokerNode.getAttribute('aria-describedby')).to.contain(el._helpTextNode.id); expect(el._invokerNode.getAttribute('aria-describedby')).to.contain(el._feedbackNode.id); expect(el._invokerNode.getAttribute('aria-haspopup')).to.equal('listbox'); }); it('notifies when the listbox is expanded or not', async () => { // smoke test for overlay functionality const el = await fixture(html` `); expect(el._invokerNode.getAttribute('aria-expanded')).to.equal('false'); el.opened = true; await el.updateComplete; await el.updateComplete; // need 2 awaits as overlay.show is an async function expect(el._invokerNode.getAttribute('aria-expanded')).to.equal('true'); }); }); describe('Use cases', () => { it('works for complex array data', async () => { const objs = [ { type: 'mastercard', label: 'Master Card', amount: 12000, active: true }, { type: 'visacard', label: 'Visa Card', amount: 0, active: false }, ]; const el = await fixture(html` ${objs.map( obj => html` ${obj.label} `, )} `); expect(el.checkedValue).to.deep.equal({ type: 'mastercard', label: 'Master Card', amount: 12000, active: true, }); el.checkedIndex = 1; expect(el.checkedValue).to.deep.equal({ type: 'visacard', label: 'Visa Card', amount: 0, active: false, }); }); it('keeps showing the selected item after a new item has been added in the selectedIndex position', async () => { const mySelectContainerTagString = defineCE( class extends LitElement { static get properties() { return { colorList: Array, }; } constructor() { super(); this.colorList = []; } render() { return html` ${this.colorList.map( colorObj => html` ${colorObj.label} `, )} `; } }, ); const mySelectContainerTag = unsafeStatic(mySelectContainerTagString); const el = await fixture(html` <${mySelectContainerTag}> `); const colorList = [ { label: 'Red', value: 'red', checked: false, }, { label: 'Hotpink', value: 'hotpink', checked: true, }, { label: 'Teal', value: 'teal', checked: false, }, ]; el.colorList = colorList; el.requestUpdate(); await el.updateComplete; const selectRich = el.shadowRoot.querySelector('lion-select-rich'); const invoker = selectRich._invokerNode; // needed to properly set the checkedIndex and checkedValue selectRich.requestUpdate(); await selectRich.updateComplete; expect(selectRich.checkedIndex).to.equal(1); expect(selectRich.checkedValue).to.equal('hotpink'); expect(invoker.selectedElement.value).to.equal('hotpink'); colorList.splice(1, 0, { label: 'Blue', value: 'blue', checked: false, }); el.requestUpdate(); await el.updateComplete; expect(selectRich.checkedIndex).to.equal(2); expect(selectRich.checkedValue).to.equal('hotpink'); expect(invoker.selectedElement.value).to.equal('hotpink'); }); }); describe('Subclassers', () => { it('allows to override the type of overlay', async () => { const mySelectTagString = defineCE( class MySelect extends LionSelectRich { _defineOverlay({ invokerNode, contentNode }) { const ctrl = new OverlayController({ placementMode: 'global', contentNode, invokerNode, }); this.addEventListener('switch', () => { ctrl.updateConfig({ placementMode: 'local' }); }); return ctrl; } }, ); const mySelectTag = unsafeStatic(mySelectTagString); const el = await fixture(html` <${mySelectTag} label="Favorite color" name="color"> ${Array(2).map( (_, i) => html` value ${i} `, )} `); expect(el._overlayCtrl.placementMode).to.equal('global'); el.dispatchEvent(new Event('switch')); expect(el._overlayCtrl.placementMode).to.equal('local'); }); }); });