import { expect, fixture, html, unsafeStatic, triggerFocusFor, triggerBlurFor, aTimeout, } from '@open-wc/testing'; import { unsafeHTML } from '@lion/core'; import sinon from 'sinon'; import { localize } from '@lion/localize'; import { localizeTearDown } from '@lion/localize/test-helpers.js'; import '../lion-field.js'; const nameSuffix = ''; const tagString = 'lion-field'; const tag = unsafeStatic(tagString); const inputSlotString = ''; const inputSlot = unsafeHTML(inputSlotString); function mimicUserInput(formControl, newViewValue) { formControl.value = newViewValue; // eslint-disable-line no-param-reassign formControl.inputElement.dispatchEvent(new CustomEvent('input', { bubbles: true })); } beforeEach(() => { localizeTearDown(); }); describe('', () => { it(`puts a unique id "${tagString}-[hash]" on the native input`, async () => { const lionField = await fixture(`<${tagString}>${inputSlotString}`); expect(lionField.$$slot('input').id).to.equal(lionField._inputId); }); it('fires focus/blur event on host and native input if focused/blurred', async () => { const lionField = await fixture(`<${tagString}>${inputSlotString}`); const cbFocusHost = sinon.spy(); lionField.addEventListener('focus', cbFocusHost); const cbFocusNativeInput = sinon.spy(); lionField.inputElement.addEventListener('focus', cbFocusNativeInput); const cbBlurHost = sinon.spy(); lionField.addEventListener('blur', cbBlurHost); const cbBlurNativeInput = sinon.spy(); lionField.inputElement.addEventListener('blur', cbBlurNativeInput); await triggerFocusFor(lionField); expect(document.activeElement).to.equal(lionField.inputElement); expect(cbFocusHost.callCount).to.equal(1); expect(cbFocusNativeInput.callCount).to.equal(1); expect(cbBlurHost.callCount).to.equal(0); expect(cbBlurNativeInput.callCount).to.equal(0); await triggerBlurFor(lionField); expect(cbBlurHost.callCount).to.equal(1); expect(cbBlurNativeInput.callCount).to.equal(1); await triggerFocusFor(lionField); expect(document.activeElement).to.equal(lionField.inputElement); expect(cbFocusHost.callCount).to.equal(2); expect(cbFocusNativeInput.callCount).to.equal(2); await triggerBlurFor(lionField); expect(cbBlurHost.callCount).to.equal(2); expect(cbBlurNativeInput.callCount).to.equal(2); }); it('has class "state-focused" if focused', async () => { const el = await fixture(`<${tagString}>${inputSlotString}`); expect(el.classList.contains('state-focused')).to.equal(false, 'no state-focused initially'); await triggerFocusFor(el.inputElement); expect(el.classList.contains('state-focused')).to.equal(true, 'state-focused after focus()'); await triggerBlurFor(el.inputElement); expect(el.classList.contains('state-focused')).to.equal(false, 'no state-focused after blur()'); }); it('offers simple getter "this.focused" returning true/false for the current focus state', async () => { const lionField = await fixture(`<${tagString}>${inputSlotString}`); expect(lionField.focused).to.equal(false); await triggerFocusFor(lionField); expect(lionField.focused).to.equal(true); await triggerBlurFor(lionField); expect(lionField.focused).to.equal(false); }); it('can be disabled via attribute', async () => { const lionFieldDisabled = await fixture( `<${tagString} disabled>${inputSlotString}`, ); expect(lionFieldDisabled.disabled).to.equal(true); expect(lionFieldDisabled.inputElement.disabled).to.equal(true); }); it('can be disabled via property', async () => { const lionField = await fixture(`<${tagString}>${inputSlotString}`); lionField.disabled = true; await lionField.updateComplete; expect(lionField.inputElement.disabled).to.equal(true); }); it('can be cleared which erases value, validation and interaction states', async () => { const lionField = await fixture( `<${tagString} value="Some value from attribute">${inputSlotString}`, ); lionField.clear(); expect(lionField.value).to.equal(''); lionField.value = 'Some value from property'; expect(lionField.value).to.equal('Some value from property'); lionField.clear(); expect(lionField.value).to.equal(''); }); it('reads initial value from attribute value', async () => { const lionField = await fixture(`<${tagString} value="one">${inputSlotString}`); expect(lionField.$$slot('input').value).to.equal('one'); }); it('delegates value property', async () => { const lionField = await fixture(`<${tagString}>${inputSlotString}`); expect(lionField.$$slot('input').value).to.equal(''); lionField.value = 'one'; expect(lionField.value).to.equal('one'); expect(lionField.$$slot('input').value).to.equal('one'); }); // TODO: find out if we could put all listeners on this.value (instead of this.inputElement.value) // and make it act on this.value again it('has a class "state-filled" if this.value is filled', async () => { const lionField = await fixture( `<${tagString} value="filled">${inputSlotString}`, ); expect(lionField.classList.contains('state-filled')).to.equal(true); lionField.value = ''; await lionField.updateComplete; expect(lionField.classList.contains('state-filled')).to.equal(false); lionField.value = 'bla'; await lionField.updateComplete; expect(lionField.classList.contains('state-filled')).to.equal(true); }); it('preserves the caret position on value change for native text fields (input|textarea)', async () => { const lionField = await fixture(`<${tagString}>${inputSlotString}`); await triggerFocusFor(lionField); await lionField.updateComplete; lionField.inputElement.value = 'hello world'; lionField.inputElement.selectionStart = 2; lionField.inputElement.selectionEnd = 2; lionField.value = 'hey there universe'; expect(lionField.inputElement.selectionStart).to.equal(2); expect(lionField.inputElement.selectionEnd).to.equal(2); }); // TODO: add pointerEvents test for disabled it('has a class "state-disabled"', async () => { const lionField = await fixture(`<${tagString}>${inputSlotString}`); expect(lionField.classList.contains('state-disabled')).to.equal(false); expect(lionField.inputElement.hasAttribute('disabled')).to.equal(false); lionField.disabled = true; await lionField.updateComplete; await aTimeout(); expect(lionField.classList.contains('state-disabled')).to.equal(true); expect(lionField.inputElement.hasAttribute('disabled')).to.equal(true); const disabledlionField = await fixture( `<${tagString} disabled>${inputSlotString}`, ); expect(disabledlionField.classList.contains('state-disabled')).to.equal(true); expect(disabledlionField.inputElement.hasAttribute('disabled')).to.equal(true); }); describe(`A11y${nameSuffix}`, () => { it(`by setting corresponding aria-labelledby (for label) and aria-describedby (for helpText, feedback) ~~~ [helpText]
[feedback] ~~~`, async () => { const lionField = await fixture(`<${tagString}> ${inputSlotString} Enter your Name No name entered `); const nativeInput = lionField.$$slot('input'); expect(nativeInput.getAttribute('aria-labelledby')).to.equal(` label-${lionField._inputId}`); expect(nativeInput.getAttribute('aria-describedby')).to.contain( ` help-text-${lionField._inputId}`, ); expect(nativeInput.getAttribute('aria-describedby')).to.contain( ` feedback-${lionField._inputId}`, ); }); it(`allows additional slots (prefix, suffix, before, after) to be included in labelledby (via attribute data-label) and in describedby (via attribute data-description)`, async () => { const lionField = await fixture(`<${tagString}> ${inputSlotString} [before] [after] [prefix] [suffix] `); const nativeInput = lionField.$$slot('input'); expect(nativeInput.getAttribute('aria-labelledby')).to.contain( ` before-${lionField._inputId} after-${lionField._inputId}`, ); expect(nativeInput.getAttribute('aria-describedby')).to.contain( ` prefix-${lionField._inputId} suffix-${lionField._inputId}`, ); }); // TODO: put this test on FormControlMixin test once there it(`allows to add to aria description or label via addToAriaLabel() and addToAriaDescription()`, async () => { const wrapper = await fixture(`
<${tagString}> ${inputSlotString}
Added to description by default
This also needs to be read whenever the input has focus
Same for this
`); const el = wrapper.querySelector(`${tagString}`); // wait until the field element is done rendering await el.updateComplete; const { inputElement } = el; const get = by => inputElement.getAttribute(`aria-${by}`); // 1. addToAriaLabel() // Check if the aria attr is filled initially expect(get('labelledby')).to.contain(`label-${el._inputId}`); el.addToAriaLabel('additionalLabel'); // Now check if ids are added to the end (not overridden) expect(get('labelledby')).to.contain(`label-${el._inputId}`); // Should be placed in the end expect( get('labelledby').indexOf(`label-${el._inputId}`) < get('labelledby').indexOf('additionalLabel'), ); // 2. addToAriaDescription() // Check if the aria attr is filled initially expect(get('describedby')).to.contain(`feedback-${el._inputId}`); el.addToAriaDescription('additionalDescription'); // Now check if ids are added to the end (not overridden) expect(get('describedby')).to.contain(`feedback-${el._inputId}`); // Should be placed in the end expect( get('describedby').indexOf(`feedback-${el._inputId}`) < get('describedby').indexOf('additionalDescription'), ); }); }); describe(`Validation${nameSuffix}`, () => { beforeEach(() => { // Reset and preload validation translations localizeTearDown(); localize.addData('en-GB', 'lion-validate', { error: { hasX: 'This is error message for hasX', }, }); }); it('shows validity states(error|warning|info|success) when interaction criteria met ', async () => { // TODO: in order to make this test work as an integration test, we chose a modelValue // that is compatible with lion-input-email. // However, when we can put priorities to validators (making sure error message of hasX is // shown instead of a predefined validator like isEmail), we should fix this. function hasX(str) { return { hasX: str.indexOf('x') > -1 }; } const lionField = await fixture(`<${tagString}>${inputSlotString}`); const feedbackEl = lionField._feedbackElement; lionField.modelValue = 'a@b.nl'; lionField.errorValidators = [[hasX]]; expect(lionField.error.hasX).to.equal(true); expect(feedbackEl.innerText.trim()).to.equal( '', 'shows no feedback, although the element has an error', ); lionField.dirty = true; lionField.touched = true; lionField.modelValue = 'ab@c.nl'; // retrigger validation await lionField.updateComplete; expect(feedbackEl.innerText.trim()).to.equal( 'This is error message for hasX', 'shows feedback, because touched=true and dirty=true', ); lionField.touched = false; lionField.dirty = false; lionField.prefilled = true; await lionField.updateComplete; expect(feedbackEl.innerText.trim()).to.equal( 'This is error message for hasX', 'shows feedback, because prefilled=true', ); }); it('can be required', async () => { const lionField = await fixture(html` <${tag} .errorValidators=${[['required']]} >${inputSlot} `); expect(lionField.error.required).to.be.true; lionField.modelValue = 'cat'; expect(lionField.error.required).to.be.undefined; }); it('will only update formattedValue when valid on `user-input-changed`', async () => { const formatterSpy = sinon.spy(value => `foo: ${value}`); function isBarValidator(value) { return { isBar: value === 'bar' }; } const lionField = await fixture(html` <${tag} .modelValue=${'init-string'} .formatter=${formatterSpy} .errorValidators=${[[isBarValidator]]} >${inputSlot} `); expect(formatterSpy.callCount).to.equal(0); expect(lionField.formattedValue).to.equal('init-string'); lionField.modelValue = 'bar'; expect(formatterSpy.callCount).to.equal(1); expect(lionField.formattedValue).to.equal('foo: bar'); mimicUserInput(lionField, 'foo'); expect(formatterSpy.callCount).to.equal(1); expect(lionField.value).to.equal('foo'); }); }); describe(`Content projection${nameSuffix}`, () => { it('renders correctly all slot elements in light DOM', async () => { const lionField = await fixture(` <${tagString}> ${inputSlotString} [help-text] [before] [after] [prefix] [suffix] [feedback] `); const names = [ 'label', 'input', 'help-text', 'before', 'after', 'prefix', 'suffix', 'feedback', ]; names.forEach(slotName => { lionField.querySelector(`[slot="${slotName}"]`).setAttribute('test-me', 'ok'); const slot = lionField.shadowRoot.querySelector(`slot[name="${slotName}"]`); const assignedNodes = slot.assignedNodes(); expect(assignedNodes.length).to.equal(1); expect(assignedNodes[0].getAttribute('test-me')).to.equal('ok'); }); }); }); describe(`Delegation${nameSuffix}`, () => { it('delegates attribute autofocus', async () => { const el = await fixture(`<${tagString} autofocus>${inputSlotString}`); expect(el.hasAttribute('autofocus')).to.be.false; expect(el.inputElement.hasAttribute('autofocus')).to.be.true; }); it('delegates property value', async () => { const el = await fixture(`<${tagString}>${inputSlotString}`); expect(el.inputElement.value).to.equal(''); el.value = 'one'; expect(el.value).to.equal('one'); expect(el.inputElement.value).to.equal('one'); }); it('delegates property type', async () => { const el = await fixture(`<${tagString} type="text">${inputSlotString}`); const inputElemTag = el.inputElement.tagName.toLowerCase(); if (inputElemTag === 'select') { // TODO: later on we might want to support multi select ? expect(el.inputElement.type).to.contain('select-one'); } else if (inputElemTag === 'textarea') { expect(el.inputElement.type).to.contain('textarea'); } else { // input or custom inputElement expect(el.inputElement.type).to.contain('text'); el.type = 'password'; expect(el.type).to.equal('password'); expect(el.inputElement.type).to.equal('password'); } }); it('delegates property onfocus', async () => { const el = await fixture(`<${tagString}>${inputSlotString}`); const cbFocusHost = sinon.spy(); el.onfocus = cbFocusHost; await triggerFocusFor(el.inputElement); expect(cbFocusHost.callCount).to.equal(1); }); it('delegates property onblur', async () => { const el = await fixture(`<${tagString}>${inputSlotString}`); const cbBlurHost = sinon.spy(); el.onblur = cbBlurHost; await triggerFocusFor(el.inputElement); await triggerBlurFor(el.inputElement); expect(cbBlurHost.callCount).to.equal(1); }); it('delegates property selectionStart and selectionEnd', async () => { const lionField = await fixture(html` <${tag} .modelValue=${'Some text to select'} >${unsafeHTML(inputSlotString)} `); lionField.selectionStart = 5; lionField.selectionEnd = 12; expect(lionField.inputElement.selectionStart).to.equal(5); expect(lionField.inputElement.selectionEnd).to.equal(12); }); }); });