import { expect, fixture, html, triggerFocusFor, triggerBlurFor } from '@open-wc/testing'; import './keyboardEventShimIE.js'; import '@lion/option/lion-option.js'; import '../lion-options.js'; import '../lion-select-rich.js'; describe('lion-select-rich interactions', () => { describe('values', () => { it('registers options', async () => { const el = await fixture(html` Item 1 Item 2 `); expect(el.formElements.length).to.equal(2); expect(el.formElements).to.eql([ el.querySelectorAll('lion-option')[0], el.querySelectorAll('lion-option')[1], ]); }); it('has the first element by default checked and active', async () => { const el = await fixture(html` Item 1 Item 2 `); expect(el.querySelector('lion-option').checked).to.be.true; expect(el.querySelector('lion-option').active).to.be.true; expect(el.modelValue).to.deep.equal([ { value: 10, checked: true }, { value: 20, checked: false }, ]); expect(el.checkedValue).to.equal(10); expect(el.checkedIndex).to.equal(0); expect(el.activeIndex).to.equal(0); }); it('allows null choiceValue', async () => { const el = await fixture(html` Please select value Item 2 `); expect(el.modelValue).to.deep.equal([ { value: null, checked: true }, { value: 20, checked: false }, ]); expect(el.checkedValue).to.be.null; }); it('has the checked option as modelValue', async () => { const el = await fixture(html` Item 1 Item 2 `); expect(el.modelValue).to.deep.equal([ { value: 10, checked: false }, { value: 20, checked: true }, ]); expect(el.checkedValue).to.equal(20); }); it('syncs checkedValue to modelValue', async () => { const el = await fixture(html` Item 1 Item 2 `); el.checkedValue = 20; expect(el.modelValue).to.deep.equal([ { value: 10, checked: false }, { value: 20, checked: true }, ]); }); it('has an activeIndex', async () => { const el = await fixture(html` Item 1 Item 2 `); expect(el.activeIndex).to.equal(0); el.querySelectorAll('lion-option')[1].active = true; expect(el.querySelectorAll('lion-option')[0].active).to.be.false; expect(el.activeIndex).to.equal(1); }); }); describe('Keyboard navigation', () => { it('does not allow to navigate above the first or below the last option', async () => { const el = await fixture(html` Item 1 `); expect(() => { el.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowUp' })); el.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' })); }).to.not.throw(); expect(el.checkedIndex).to.equal(0); expect(el.activeIndex).to.equal(0); }); it('navigates to first and last option with [Home] and [End] keys', async () => { const el = await fixture(html` Item 1 Item 2 Item 3 Item 4 `); expect(el.checkedValue).to.equal(30); el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Home' })); expect(el.checkedValue).to.equal(10); el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'End' })); expect(el.checkedValue).to.equal(40); }); // TODO: nice to have it.skip('selects a value with single [character] key', async () => { const el = await fixture(html` A B C `); expect(el.choiceValue).to.equal('a'); el.dispatchEvent(new KeyboardEvent('keyup', { key: 'C' })); expect(el.choiceValue).to.equal('c'); }); it.skip('selects a value with multiple [character] keys', async () => { const el = await fixture(html` Bar Far Foo `); el.dispatchEvent(new KeyboardEvent('keyup', { key: 'F' })); expect(el.choiceValue).to.equal('far'); el.dispatchEvent(new KeyboardEvent('keyup', { key: 'O' })); expect(el.choiceValue).to.equal('foo'); }); }); describe('Keyboard navigation Windows', () => { it('navigates through list with [ArrowDown] [ArrowUp] keys activates and checks the option', async () => { function expectOnlyGivenOneOptionToBeChecked(options, selectedIndex) { options.forEach((option, i) => { if (i === selectedIndex) { expect(option.checked).to.be.true; } else { expect(option.checked).to.be.false; } }); } const el = await fixture(html` Item 1 Item 2 Item 3 `); const options = Array.from(el.querySelectorAll('lion-option')); expect(el.activeIndex).to.equal(0); expect(el.checkedIndex).to.equal(0); expectOnlyGivenOneOptionToBeChecked(options, 0); el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' })); expect(el.activeIndex).to.equal(1); expect(el.checkedIndex).to.equal(1); expectOnlyGivenOneOptionToBeChecked(options, 1); el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowUp' })); expect(el.activeIndex).to.equal(0); expect(el.checkedIndex).to.equal(0); expectOnlyGivenOneOptionToBeChecked(options, 0); }); it('navigates through list with [ArrowDown] [ArrowUp] keys checks the option while listbox unopened', async () => { function expectOnlyGivenOneOptionToBeChecked(options, selectedIndex) { options.forEach((option, i) => { if (i === selectedIndex) { expect(option.checked).to.be.true; } else { expect(option.checked).to.be.false; } }); } const el = await fixture(html` Item 1 Item 2 Item 3 `); const options = Array.from(el.querySelectorAll('lion-option')); expect(el.checkedIndex).to.equal(0); expectOnlyGivenOneOptionToBeChecked(options, 0); el.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' })); expect(el.checkedIndex).to.equal(1); expectOnlyGivenOneOptionToBeChecked(options, 1); el.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowUp' })); expect(el.checkedIndex).to.equal(0); expectOnlyGivenOneOptionToBeChecked(options, 0); }); }); describe('Keyboard navigation Mac', () => { it('navigates through open list with [ArrowDown] [ArrowUp] keys activates the option', async () => { const el = await fixture(html` Item 1 Item 2 Item 3 `); expect(el.activeIndex).to.equal(0); expect(el.checkedIndex).to.equal(0); el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' })); expect(el.activeIndex).to.equal(1); expect(el.checkedIndex).to.equal(0); el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowUp' })); expect(el.activeIndex).to.equal(0); expect(el.checkedIndex).to.equal(0); }); }); describe('Disabled', () => { it('cannot be focused if disabled', async () => { const el = await fixture(html` `); expect(el._invokerNode.tabIndex).to.equal(-1); }); it('still has a checked value', async () => { const el = await fixture(html` Item 1 Item 2 `); expect(el.checkedValue).to.equal(10); }); it('cannot be navigated with keyboard if disabled', async () => { const el = await fixture(html` Item 1 Item 2 `); el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' })); expect(el.checkedValue).to.equal(10); }); it('cannot be opened via click if disabled', async () => { const el = await fixture(html` `); el._invokerNode.click(); expect(el.opened).to.be.false; }); it('reflects disabled attribute to invoker', async () => { const el = await fixture(html` `); expect(el._invokerNode.hasAttribute('disabled')).to.be.true; el.removeAttribute('disabled'); await el.updateComplete; expect(el._invokerNode.hasAttribute('disabled')).to.be.false; }); it('skips disabled options while navigating through list with [ArrowDown] [ArrowUp] keys', async () => { const el = await fixture(html` Item 1 Item 2 Item 3 `); el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' })); expect(el.activeIndex).to.equal(2); el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowUp' })); expect(el.activeIndex).to.equal(0); }); it('skips disabled options while navigates to first and last option with [Home] and [End] keys', async () => { const el = await fixture(html` Item 1 Item 2 Item 3 Item 4 `); expect(el.activeIndex).to.equal(2); el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Home' })); expect(el.activeIndex).to.equal(1); el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'End' })); expect(el.activeIndex).to.equal(2); }); it('checks the first enabled option', async () => { const el = await fixture(html` Item 1 Item 2 Item 3 `); expect(el.activeIndex).to.equal(1); expect(el.checkedIndex).to.equal(1); }); it('sync its disabled state to all options', async () => { const el = await fixture(html` Item 1 Item 2 `); const options = [...el.querySelectorAll('lion-option')]; el.disabled = true; await el.updateComplete; expect(options[0].disabled).to.be.true; expect(options[1].disabled).to.be.true; el.disabled = false; await el.updateComplete; expect(options[0].disabled).to.be.true; expect(options[1].disabled).to.be.false; }); it('can be enabled (incl. its options) even if it starts as disabled', async () => { const el = await fixture(html` Item 1 Item 2 `); const options = [...el.querySelectorAll('lion-option')]; expect(options[0].disabled).to.be.true; expect(options[1].disabled).to.be.true; el.disabled = false; await el.updateComplete; expect(options[0].disabled).to.be.true; expect(options[1].disabled).to.be.false; }); }); // TODO: nice to have describe.skip('Read only', () => { it('can be focused if readonly', async () => { const el = await fixture(html` `); expect(el.tabIndex).to.equal('-1'); }); it('cannot be navigated with keyboard if readonly', async () => { const el = await fixture(html` Item 1 Item 2 `); el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' })); expect(el.choiceValue).to.equal(10); }); }); describe('Programmatic interaction', () => { it('can set active state', async () => { const el = await fixture(html` Item 1 Item 2 `); const opt = el.querySelectorAll('lion-option')[1]; opt.active = true; expect(el._listboxNode.getAttribute('aria-activedescendant')).to.equal('myId'); }); it('can set checked state', async () => { const el = await fixture(html` Item 1 Item 2 `); const option = el.querySelectorAll('lion-option')[1]; option.checked = true; expect(el.modelValue).to.deep.equal([ { value: 10, checked: false }, { value: 20, checked: true }, ]); }); it('does not allow to set checkedIndex or activeIndex to be out of bound', async () => { const el = await fixture(html` Item 1 `); expect(() => { el.activeIndex = -1; el.activeIndex = 1; el.checkedIndex = -1; el.checkedIndex = 1; }).to.not.throw(); expect(el.checkedIndex).to.equal(0); expect(el.activeIndex).to.equal(0); }); it('unsets checked on other options when option becomes checked', async () => { const el = await fixture(html` Item 1 Item 2 `); const options = el.querySelectorAll('lion-option'); expect(options[0].checked).to.be.true; options[1].checked = true; expect(options[0].checked).to.be.false; }); it('unsets active on other options when option becomes active', async () => { const el = await fixture(html` Item 1 Item 2 `); const options = el.querySelectorAll('lion-option'); expect(options[0].active).to.be.true; options[1].active = true; expect(options[0].active).to.be.false; }); }); describe('Interaction states', () => { it('becomes dirty if value changed once', async () => { const el = await fixture(html` Item 1 Item 2 `); expect(el.dirty).to.be.false; el.checkedValue = 20; expect(el.dirty).to.be.true; }); it('becomes touched if blurred once', async () => { const el = await fixture(html` Item 1 Item 2 `); expect(el.touched).to.be.false; await triggerFocusFor(el._invokerNode); await triggerBlurFor(el._invokerNode); expect(el.touched).to.be.true; }); it('is prefilled if there is a value on init', async () => { const el = await fixture(html` Item 1 `); expect(el.prefilled).to.be.true; const elEmpty = await fixture(html` Please select a value Item 1 `); expect(elEmpty.prefilled).to.be.false; }); }); describe('Validation', () => { it('can be required', async () => { const el = await fixture(html` Please select a value Item 2 `); expect(el.error.required).to.be.true; el.checkedValue = 20; expect(el.error.required).to.be.undefined; }); }); describe('Accessibility', () => { it('creates unique ids for all children', async () => { const el = await fixture(html` Item 1 Item 2 Item 3 `); expect(el.querySelectorAll('lion-option')[0].id).to.exist; expect(el.querySelectorAll('lion-option')[1].id).to.exist; expect(el.querySelectorAll('lion-option')[2].id).to.equal('predefined'); }); it('has a reference to the selected option', async () => { const el = await fixture(html` Item 1 Item 2 `); expect(el._listboxNode.getAttribute('aria-activedescendant')).to.equal('first'); el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' })); expect(el._listboxNode.getAttribute('aria-activedescendant')).to.equal('second'); }); it('puts "aria-setsize" on all options to indicate the total amount of options', async () => { const el = await fixture(html` Item 1 Item 2 Item 3 `); const optionEls = [].slice.call(el.querySelectorAll('lion-option')); optionEls.forEach(optionEl => { expect(optionEl.getAttribute('aria-setsize')).to.equal('3'); }); }); it('puts "aria-posinset" on all options to indicate their position in the listbox', async () => { const el = await fixture(html` Item 1 Item 2 Item 3 `); const optionEls = [].slice.call(el.querySelectorAll('lion-option')); optionEls.forEach((oEl, i) => { expect(oEl.getAttribute('aria-posinset')).to.equal(`${i + 1}`); }); }); }); });