import { aTimeout, expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing'; import { clickOnElement } from '../../internal/test'; import { sendKeys } from '@web/test-runner-commands'; import { serialize } from '../../utilities/form'; import sinon from 'sinon'; import type SlOption from '../option/option'; import type SlSelect from './select'; describe('', () => { it('should pass accessibility tests', async () => { const el = await fixture(html` Option 1 Option 2 Option 3 `); await expect(el).to.be.accessible(); }); it('should be disabled with the disabled attribute', async () => { const el = await fixture(html` Option 1 Option 2 Option 3 `); expect(el.displayInput.disabled).to.be.true; }); it('should show a placeholder when no options are selected', async () => { const el = await fixture(html` Option 1 Option 2 Option 3 `); const displayInput = el.shadowRoot!.querySelector('[part~="display-input"]')!; expect(getComputedStyle(displayInput).opacity).to.not.equal('0'); expect(displayInput.placeholder).to.equal('Select one'); }); it('should show a placeholder when no options are selected and multiple is set', async () => { const el = await fixture(html` Option 1 Option 2 Option 3 `); const displayInput = el.shadowRoot!.querySelector('[part~="display-input"]')!; expect(getComputedStyle(displayInput).opacity).to.not.equal('0'); expect(displayInput.placeholder).to.equal('Select a few'); }); it('should not allow selection when the option is disabled', async () => { const el = await fixture(html` Option 1 Option 2 `); const disabledOption = el.querySelector('sl-option[disabled]')!; await el.show(); await clickOnElement(disabledOption); await el.updateComplete; expect(el.value).to.equal('option-1'); }); it('should focus the select when clicking on the label', async () => { const el = await fixture(html` Option 1 Option 2 Option 3 `); const label = el.shadowRoot!.querySelector('[part~="form-control-label"]')!; const submitHandler = sinon.spy(); el.addEventListener('sl-focus', submitHandler); (label as HTMLLabelElement).click(); await waitUntil(() => submitHandler.calledOnce); expect(submitHandler).to.have.been.calledOnce; }); describe('when the value changes', () => { it('should emit sl-change when the value is changed with the mouse', async () => { const el = await fixture(html` Option 1 Option 2 Option 3 `); const secondOption = el.querySelectorAll('sl-option')[1]; const changeHandler = sinon.spy(); const inputHandler = sinon.spy(); el.addEventListener('sl-change', changeHandler); el.addEventListener('sl-input', inputHandler); await el.show(); await clickOnElement(secondOption); await el.updateComplete; expect(changeHandler).to.have.been.calledOnce; expect(inputHandler).to.have.been.calledOnce; expect(el.value).to.equal('option-2'); }); it('should emit sl-change and sl-input when the value is changed with the keyboard', async () => { const el = await fixture(html` Option 1 Option 2 Option 3 `); const changeHandler = sinon.spy(); const inputHandler = sinon.spy(); el.addEventListener('sl-change', changeHandler); el.addEventListener('sl-input', inputHandler); el.focus(); await el.updateComplete; await sendKeys({ press: ' ' }); // open the dropdown await aTimeout(500); // wait for the dropdown to open await sendKeys({ press: 'ArrowDown' }); // move selection to the second option await el.updateComplete; await sendKeys({ press: 'ArrowDown' }); // move selection to the third option await el.updateComplete; await sendKeys({ press: 'Enter' }); // commit the selection await el.updateComplete; expect(changeHandler).to.have.been.calledOnce; expect(inputHandler).to.have.been.calledOnce; expect(el.value).to.equal('option-3'); }); it('should not emit sl-change or sl-input when the value is changed programmatically', async () => { const el = await fixture(html` Option 1 Option 2 Option 3 `); el.addEventListener('sl-change', () => expect.fail('sl-change should not be emitted')); el.addEventListener('sl-input', () => expect.fail('sl-input should not be emitted')); el.value = 'option-2'; await el.updateComplete; }); }); it('should open the listbox when any letter key is pressed with sl-select is on focus', async () => { const el = await fixture(html` Option 1 Option 2 Option 3 `); const displayInput = el.shadowRoot!.querySelector('.select__display-input')!; el.focus(); await sendKeys({ press: 'r' }); await el.updateComplete; expect(displayInput.getAttribute('aria-expanded')).to.equal('true'); }); it('should not open the listbox when ctrl + R is pressed with sl-select is on focus', async () => { const el = await fixture(html` Option 1 Option 2 Option 3 `); const displayInput = el.shadowRoot!.querySelector('.select__display-input')!; el.focus(); await sendKeys({ down: 'Control' }); await sendKeys({ press: 'r' }); await sendKeys({ up: 'Control' }); await el.updateComplete; expect(displayInput.getAttribute('aria-expanded')).to.equal('false'); }); describe('when using constraint validation', () => { it('should be valid by default', async () => { const el = await fixture(html`
Option 1 Option 2 Option 3
`); const select = el.querySelector('sl-select')!; expect(select.checkValidity()).to.be.true; }); it('should be invalid when required and empty', async () => { const el = await fixture(html`
Option 1 Option 2 Option 3
`); const select = el.querySelector('sl-select')!; expect(select.checkValidity()).to.be.false; }); it('should focus on the displayInput when constraint validation occurs', async () => { const el = await fixture(html`
Option 1 Option 2 Option 3
`); const select = el.querySelector('sl-select')!; el.requestSubmit(); expect(select.shadowRoot!.activeElement).to.equal(select.displayInput); }); it('should receive the correct validation attributes ("states") when valid', async () => { const el = await fixture(html` Option 1 Option 2 Option 3 `); const secondOption = el.querySelectorAll('sl-option')[1]!; expect(el.checkValidity()).to.be.true; expect(el.hasAttribute('data-required')).to.be.true; expect(el.hasAttribute('data-optional')).to.be.false; expect(el.hasAttribute('data-invalid')).to.be.false; expect(el.hasAttribute('data-valid')).to.be.true; expect(el.hasAttribute('data-user-invalid')).to.be.false; expect(el.hasAttribute('data-user-valid')).to.be.false; await el.show(); await clickOnElement(secondOption); await el.updateComplete; expect(el.checkValidity()).to.be.true; expect(el.hasAttribute('data-user-invalid')).to.be.false; expect(el.hasAttribute('data-user-valid')).to.be.true; }); it('should receive the correct validation attributes ("states") when invalid', async () => { const el = await fixture(html` Option 1 Option 2 Option 3 `); const secondOption = el.querySelectorAll('sl-option')[1]!; expect(el.hasAttribute('data-required')).to.be.true; expect(el.hasAttribute('data-optional')).to.be.false; expect(el.hasAttribute('data-invalid')).to.be.true; expect(el.hasAttribute('data-valid')).to.be.false; expect(el.hasAttribute('data-user-invalid')).to.be.false; expect(el.hasAttribute('data-user-valid')).to.be.false; await el.show(); await clickOnElement(secondOption); el.value = ''; await el.updateComplete; expect(el.hasAttribute('data-user-invalid')).to.be.true; expect(el.hasAttribute('data-user-valid')).to.be.false; }); }); describe('when submitting a form', () => { it('should serialize its name and value with FormData', async () => { const form = await fixture(html`
Option 1 Option 2 Option 3
`); const formData = new FormData(form); expect(formData.get('a')).to.equal('option-1'); }); it('should serialize its name and value in FormData when multiple options are selected', async () => { const form = await fixture(html`
Option 1 Option 2 Option 3
`); const formData = new FormData(form); expect(formData.getAll('a')).to.include('option-2'); expect(formData.getAll('a')).to.include('option-3'); }); it('should serialize its name and value in JSON', async () => { const form = await fixture(html`
Option 1 Option 2 Option 3
`); const json = serialize(form); expect(json.a).to.equal('option-1'); }); it('should serialize its name and value in JSON when multiple options are selected', async () => { const form = await fixture(html`
Option 1 Option 2 Option 3
`); const json = serialize(form); expect(JSON.stringify(json)).to.equal(JSON.stringify({ a: ['option-2', 'option-3'] })); }); it('should be present in form data when using the form attribute and located outside of a
', async () => { const el = await fixture(html`
Submit Option 1 Option 2 Option 3
`); const form = el.querySelector('form')!; const formData = new FormData(form); expect(formData.get('a')).to.equal('option-1'); }); }); describe('when resetting a form', () => { it('should reset the element to its initial value', async () => { const form = await fixture(html`
Option 1 Option 2 Option 3 Reset
`); const resetButton = form.querySelector('sl-button')!; const select = form.querySelector('sl-select')!; select.value = 'option-3'; await select.updateComplete; expect(select.value).to.equal('option-3'); setTimeout(() => clickOnElement(resetButton)); await oneEvent(form, 'reset'); await select.updateComplete; expect(select.value).to.equal('option-1'); }); }); it('should update the display label when an option changes', async () => { const el = await fixture(html` Option 1 Option 2 Option 3 `); const displayInput = el.shadowRoot!.querySelector('.select__display-input')!; const option = el.querySelector('sl-option')!; expect(displayInput.value).to.equal('Option 1'); option.textContent = 'updated'; await oneEvent(option, 'slotchange'); await el.updateComplete; expect(displayInput.value).to.equal('updated'); }); it('should emit sl-focus and sl-blur when receiving and losing focus', async () => { const el = await fixture(html` Option 1 Option 2 Option 3 `); const focusHandler = sinon.spy(); const blurHandler = sinon.spy(); el.addEventListener('sl-focus', focusHandler); el.addEventListener('sl-blur', blurHandler); el.focus(); await el.updateComplete; el.blur(); await el.updateComplete; expect(focusHandler).to.have.been.calledOnce; expect(blurHandler).to.have.been.calledOnce; }); it('should emit sl-clear when the clear button is clicked', async () => { const el = await fixture(html` Option 1 Option 2 Option 3 `); const clearHandler = sinon.spy(); const clearButton = el.shadowRoot!.querySelector('[part~="clear-button"]')!; el.addEventListener('sl-clear', clearHandler); await el.show(); await clickOnElement(clearButton); await el.updateComplete; expect(clearHandler).to.have.been.calledOnce; }); it('should emit sl-change and sl-input when a tag is removed', async () => { const el = await fixture(html` Option 1 Option 2 Option 3 `); const changeHandler = sinon.spy(); const inputHandler = sinon.spy(); const tag = el.shadowRoot!.querySelector('[part~="tag"]')!; const removeButton = tag.shadowRoot!.querySelector('[part~="remove-button"]')!; el.addEventListener('sl-change', changeHandler); el.addEventListener('sl-input', inputHandler); await clickOnElement(removeButton); await el.updateComplete; expect(changeHandler).to.have.been.calledOnce; expect(inputHandler).to.have.been.calledOnce; }); it('should emit sl-show, sl-after-show, sl-hide, and sl-after-hide events when the listbox opens and closes', async () => { const el = await fixture(html` Option 1 Option 2 Option 3 `); const showHandler = sinon.spy(); const afterShowHandler = sinon.spy(); const hideHandler = sinon.spy(); const afterHideHandler = sinon.spy(); el.addEventListener('sl-show', showHandler); el.addEventListener('sl-after-show', afterShowHandler); el.addEventListener('sl-hide', hideHandler); el.addEventListener('sl-after-hide', afterHideHandler); await el.show(); expect(showHandler).to.have.been.calledOnce; expect(afterShowHandler).to.have.been.calledOnce; await el.hide(); expect(hideHandler).to.have.been.calledOnce; expect(afterHideHandler).to.have.been.calledOnce; }); it('should have rounded tags when using the pill attribute', async () => { const el = await fixture(html` Option 1 Option 2 Option 3 `); const tag = el.shadowRoot!.querySelector('[part~="tag"]')!; expect(tag.hasAttribute('pill')).to.be.true; }); });