import { LionField } from '@lion/field'; import '@lion/field/lion-field.js'; import { formFixture as fixture } from '@lion/field/test-helpers.js'; import { localizeTearDown } from '@lion/localize/test-helpers.js'; import { IsNumber, Validator } from '@lion/validate'; import { defineCE, expect, fixtureSync, html, nextFrame, triggerFocusFor, unsafeStatic, } from '@open-wc/testing'; import sinon from 'sinon'; import '../lion-fieldset.js'; const childTagString = defineCE( class extends LionField { get slots() { return { input: () => document.createElement('input'), }; } }, ); const tagString = 'lion-fieldset'; const tag = unsafeStatic(tagString); const childTag = unsafeStatic(childTagString); const inputSlots = html` <${childTag} name="gender[]"> <${childTag} name="gender[]"> <${childTag} name="color"> <${childTag} name="hobbies[]"> <${childTag} name="hobbies[]"> `; beforeEach(() => { localizeTearDown(); }); // TODO: seperate fieldset and FormGroup tests describe('', () => { // TODO: Tests below belong to FormControlMixin. Preferably run suite integration test it(`has a fieldName based on the label`, async () => { const el1 = await fixture(html`<${tag} label="foo">${inputSlots}`); expect(el1.fieldName).to.equal(el1._labelNode.textContent); const el2 = await fixture(html`<${tag}>${inputSlots}`); 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`<${tag} name="foo">${inputSlots}`); expect(el.fieldName).to.equal(el.name); }); it(`can override fieldName`, async () => { const el = await fixture( html`<${tag} label="foo" .fieldName="${'bar'}">${inputSlots}`, ); expect(el.__fieldName).to.equal(el.fieldName); }); // TODO: Tests below belong to FormRegistrarMixin. Preferably run suite integration test it(`${tagString} has an up to date list of every form element in .formElements`, async () => { const el = await fixture(html`<${tag}>${inputSlots}`); await nextFrame(); expect(el.formElements.keys().length).to.equal(3); expect(el.formElements['hobbies[]'].length).to.equal(2); el.removeChild(el.formElements['hobbies[]'][0]); expect(el.formElements.keys().length).to.equal(3); expect(el.formElements['hobbies[]'].length).to.equal(1); }); it(`supports in html wrapped form elements`, async () => { const el = await fixture(html` <${tag}>
<${childTag} name="foo">
`); await nextFrame(); expect(el.formElements.length).to.equal(1); el.children[0].removeChild(el.formElements.foo); expect(el.formElements.length).to.equal(0); }); it('handles names with ending [] as an array', async () => { const el = await fixture(html`<${tag}>${inputSlots}`); await nextFrame(); el.formElements['gender[]'][0].modelValue = { value: 'male' }; el.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' }; el.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' }; expect(el.formElements.keys().length).to.equal(3); expect(el.formElements['hobbies[]'].length).to.equal(2); expect(el.formElements['hobbies[]'][0].modelValue.value).to.equal('chess'); expect(el.formElements['gender[]'][0].modelValue.value).to.equal('male'); expect(el.modelValue['hobbies[]']).to.deep.equal([ { checked: false, value: 'chess' }, { checked: false, value: 'rugby' }, ]); }); it('throws if an element without a name tries to register', async () => { const orig = console.info; console.info = () => {}; let error = false; const el = await fixture(html`<${tag}>`); try { // we test the api directly as errors thrown from a web component are in a // different context and we can not catch them here => register fake elements el.addFormElement({}); } catch (err) { error = err; } expect(error).to.be.instanceOf(TypeError); expect(error.message).to.equal('You need to define a name'); console.info = orig; // restore original console }); it('throws if name is the same as its parent', async () => { const orig = console.info; console.info = () => {}; let error = false; const el = await fixture(html`<${tag} name="foo">`); try { // we test the api directly as errors thrown from a web component are in a // different context and we can not catch them here => register fake elements el.addFormElement({ name: 'foo' }); } catch (err) { error = err; } expect(error).to.be.instanceOf(TypeError); expect(error.message).to.equal('You can not have the same name "foo" as your parent'); console.info = orig; // restore original console }); it('throws if same name without ending [] is used', async () => { const orig = console.info; console.info = () => {}; let error = false; const el = await fixture(html`<${tag}>`); try { // we test the api directly as errors thrown from a web component are in a // different context and we can not catch them here => register fake elements el.addFormElement({ name: 'fooBar' }); el.addFormElement({ name: 'fooBar' }); } catch (err) { error = err; } expect(error).to.be.instanceOf(TypeError); expect(error.message).to.equal( 'Name "fooBar" is already registered - if you want an array add [] to the end', ); console.info = orig; // restore original console }); /* eslint-enable no-console */ it('can dynamically add/remove elements', async () => { const el = await fixture(html`<${tag}>${inputSlots}`); const newField = await fixture(html`<${childTag} name="lastName">`); expect(el.formElements.keys().length).to.equal(3); el.appendChild(newField); expect(el.formElements.keys().length).to.equal(4); el._inputNode.removeChild(newField); expect(el.formElements.keys().length).to.equal(3); }); // TODO: Tests below belong to FormGroupMixin. Preferably run suite integration test it('can read/write all values (of every input) via this.modelValue', async () => { const el = await fixture(html` <${tag}> <${childTag} name="lastName"> <${tag} name="newfieldset">${inputSlots} `); await el.registrationReady; const newFieldset = el.querySelector('lion-fieldset'); await newFieldset.registrationReady; el.formElements.lastName.modelValue = 'Bar'; newFieldset.formElements['hobbies[]'][0].modelValue = { checked: true, value: 'chess' }; newFieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'football' }; newFieldset.formElements['gender[]'][0].modelValue = { checked: false, value: 'male' }; newFieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' }; newFieldset.formElements.color.modelValue = { checked: false, value: 'blue' }; expect(el.modelValue).to.deep.equal({ lastName: 'Bar', newfieldset: { 'hobbies[]': [ { checked: true, value: 'chess' }, { checked: false, value: 'football' }, ], 'gender[]': [ { checked: false, value: 'male' }, { checked: false, value: 'female' }, ], color: { checked: false, value: 'blue' }, }, }); el.modelValue = { lastName: 2, newfieldset: { 'hobbies[]': [ { checked: true, value: 'chess' }, { checked: false, value: 'baseball' }, ], 'gender[]': [ { checked: false, value: 'male' }, { checked: false, value: 'female' }, ], color: { checked: false, value: 'blue' }, }, }; expect(newFieldset.formElements['hobbies[]'][0].modelValue).to.deep.equal({ checked: true, value: 'chess', }); expect(newFieldset.formElements['hobbies[]'][1].modelValue).to.deep.equal({ checked: false, value: 'baseball', }); expect(el.formElements.lastName.modelValue).to.equal(2); }); it('does not list disabled values in this.modelValue', async () => { const el = await fixture(html` <${tag}> <${childTag} name="a" disabled .modelValue="${'x'}"> <${childTag} name="b" .modelValue="${'x'}"> <${tag} name="newFieldset"> <${childTag} name="c" .modelValue="${'x'}"> <${childTag} name="d" disabled .modelValue="${'x'}"> <${tag} name="disabledFieldset" disabled> <${childTag} name="e" .modelValue="${'x'}"> `); await el.registrationReady; const newFieldset = el.querySelector('lion-fieldset'); await newFieldset.registrationReady; expect(el.modelValue).to.deep.equal({ b: 'x', newFieldset: { c: 'x', }, }); }); it('does not throw if setter data of this.modelValue can not be handled', async () => { const el = await fixture(html` <${tag}> <${childTag} name="firstName" .modelValue=${'foo'}> <${childTag} name="lastName" .modelValue=${'bar'}> `); await nextFrame(); const initState = { firstName: 'foo', lastName: 'bar', }; expect(el.modelValue).to.deep.equal(initState); el.modelValue = undefined; expect(el.modelValue).to.deep.equal(initState); el.modelValue = null; expect(el.modelValue).to.deep.equal(initState); }); it('disables/enables all its formElements if it becomes disabled/enabled', async () => { const el = await fixture(html`<${tag} disabled>${inputSlots}`); await nextFrame(); expect(el.formElements.color.disabled).to.be.true; expect(el.formElements['hobbies[]'][0].disabled).to.be.true; expect(el.formElements['hobbies[]'][1].disabled).to.be.true; el.disabled = false; await el.updateComplete; expect(el.formElements.color.disabled).to.equal(false); expect(el.formElements['hobbies[]'][0].disabled).to.equal(false); expect(el.formElements['hobbies[]'][1].disabled).to.equal(false); }); it('does not propagate/override initial disabled value on nested form elements', async () => { const el = await fixture( html`<${tag}><${tag} name="sub" disabled>${inputSlots}`, ); await el.updateComplete; expect(el.disabled).to.equal(false); expect(el.formElements.sub.disabled).to.be.true; expect(el.formElements.sub.formElements.color.disabled).to.be.true; expect(el.formElements.sub.formElements['hobbies[]'][0].disabled).to.be.true; expect(el.formElements.sub.formElements['hobbies[]'][1].disabled).to.be.true; }); it('can set initial modelValue on creation', async () => { const initialModelValue = { lastName: 'Bar', }; const el = await fixture(html` <${tag} .modelValue=${initialModelValue}> <${childTag} name="lastName"> `); await el.registrationReady; await el.updateComplete; expect(el.modelValue).to.eql(initialModelValue); }); it('can set initial serializedValue on creation', async () => { const initialSerializedValue = { lastName: 'Bar', }; const el = await fixture(html` <${tag} .modelValue=${initialSerializedValue}> <${childTag} name="lastName"> `); await el.registrationReady; await el.updateComplete; expect(el.modelValue).to.eql(initialSerializedValue); }); describe('Validation', () => { it('validates on init', async () => { class IsCat extends Validator { static get validatorName() { return 'IsCat'; } execute(value) { const hasError = value !== 'cat'; return hasError; } } const el = await fixture(html` <${tag}> <${childTag} name="color" .validators=${[new IsCat()]} .modelValue=${'blue'} > `); await nextFrame(); expect(el.formElements.color.validationStates.error.IsCat).to.be.true; }); it('validates when a value changes', async () => { const fieldset = await fixture(html`<${tag}>${inputSlots}`); await nextFrame(); const spy = sinon.spy(fieldset, 'validate'); fieldset.formElements.color.modelValue = { checked: true, value: 'red' }; expect(spy.callCount).to.equal(1); }); it('has a special validator for all children - can be checked via this.error.FormElementsHaveNoError', async () => { class IsCat extends Validator { static get validatorName() { return 'IsCat'; } execute(value) { const hasError = value !== 'cat'; return hasError; } } const el = await fixture(html` <${tag}> <${childTag} name="color" .validators=${[new IsCat()]} .modelValue=${'blue'} > `); await nextFrame(); expect(el.validationStates.error.FormElementsHaveNoError).to.be.true; expect(el.formElements.color.validationStates.error.IsCat).to.be.true; el.formElements.color.modelValue = 'cat'; expect(el.validationStates.error).to.deep.equal({}); }); it('validates on children (de)registration', async () => { class HasEvenNumberOfChildren extends Validator { static get validatorName() { return 'HasEvenNumberOfChildren'; } execute(value) { const hasError = Object.keys(value).length % 2 !== 0; return hasError; } } const el = await fixture(html` <${tag} .validators=${[new HasEvenNumberOfChildren()]}> <${childTag} id="c1" name="c1"> `); const child2 = await fixture(html` <${childTag} name="c2"> `); await nextFrame(); expect(el.validationStates.error.HasEvenNumberOfChildren).to.be.true; el.appendChild(child2); await nextFrame(); expect(el.validationStates.error.HasEvenNumberOfChildren).to.equal(undefined); el.removeChild(child2); await nextFrame(); expect(el.validationStates.error.HasEvenNumberOfChildren).to.be.true; // Edge case: remove all children el.removeChild(el.querySelector('[id=c1]')); await nextFrame(); expect(el.validationStates.error.HasEvenNumberOfChildren).to.equal(undefined); }); }); describe('Interaction states', () => { it('has false states (dirty, touched, prefilled) on init', async () => { const fieldset = await fixture(html`<${tag}>${inputSlots}`); await nextFrame(); expect(fieldset.dirty).to.equal(false, 'dirty'); expect(fieldset.touched).to.equal(false, 'touched'); expect(fieldset.prefilled).to.equal(false, 'prefilled'); }); it('sets dirty when value changed', async () => { const fieldset = await fixture(html`<${tag}>${inputSlots}`); await nextFrame(); fieldset.formElements['hobbies[]'][0].modelValue = { checked: true, value: 'football' }; expect(fieldset.dirty).to.be.true; }); it('sets touched when last field in fieldset left after focus', async () => { const fieldset = await fixture(html`<${tag}>${inputSlots}`); await triggerFocusFor(fieldset.formElements['hobbies[]'][0]._inputNode); await triggerFocusFor( fieldset.formElements['hobbies[]'][fieldset.formElements['gender[]'].length - 1]._inputNode, ); const el = await fixture(html``); el.focus(); expect(fieldset.touched).to.be.true; }); it('sets attributes [touched][dirty]', async () => { const el = await fixture(html`<${tag}>`); el.touched = true; await el.updateComplete; expect(el).to.have.attribute('touched'); el.dirty = true; await el.updateComplete; expect(el).to.have.attribute('dirty'); }); it('becomes prefilled if all form elements are prefilled', async () => { const el = await fixture(html` <${tag}> <${childTag} name="input1" .modelValue="${'prefilled'}"> <${childTag} name="input2"> `); await nextFrame(); expect(el.prefilled).to.be.false; const el2 = await fixture(html` <${tag}> <${childTag} name="input1" .modelValue="${'prefilled'}"> <${childTag} name="input2" .modelValue="${'prefilled'}"> `); await nextFrame(); expect(el2.prefilled).to.be.true; }); it(`becomes "touched" once the last element of a group becomes blurred by keyboard interaction (e.g. tabbing through the checkbox-group)`, async () => { const el = await fixture(html` <${tag}> <${childTag} name="myGroup[]" label="Option 1" value="1"> <${childTag} name="myGroup[]" label="Option 2" value="2"> `); await nextFrame(); const button = await fixture(``); expect(el.touched).to.equal(false, 'initially, touched state is false'); el.children[2].focus(); expect(el.touched).to.equal(false, 'focus is on second checkbox'); button.focus(); expect(el.touched).to.equal( true, `focus is on element behind second checkbox (group has blurred)`, ); }); it(`becomes "touched" once the group as a whole becomes blurred via mouse interaction after keyboard interaction (e.g. focus is moved inside the group and user clicks somewhere outside the group)`, async () => { const el = await fixture(html` <${tag}> <${childTag} name="input1"> <${childTag} name="input2"> `); const el2 = await fixture(html` <${tag}> <${childTag} name="input1"> <${childTag} name="input2"> `); await nextFrame(); const outside = await fixture(html``); outside.click(); expect(el.touched, 'unfocused fieldset should stay untouched').to.be.false; el.children[1].focus(); el.children[2].focus(); expect(el.touched).to.be.false; outside.click(); // blur the group via a click outside.focus(); // a real mouse click moves focus as well expect(el.touched).to.be.true; expect(el2.touched).to.be.false; }); it('potentially shows fieldset error message on interaction change', async () => { class Input1IsTen extends Validator { static get validatorName() { return 'Input1IsTen'; } execute(value) { const hasError = value.input1 !== 10; return hasError; } } const outSideButton = await fixture(html``); const el = await fixture(html` <${tag} .validators=${[new Input1IsTen()]}> <${childTag} name="input1" .validators=${[new IsNumber()]}> `); await nextFrame(); const input1 = el.querySelector(childTagString); input1.modelValue = 2; input1.focus(); outSideButton.focus(); await el.updateComplete; expect(el.validationStates.error.Input1IsTen).to.be.true; expect(el.showsFeedbackFor).to.deep.equal(['error']); }); it('show error if tabbing "out" of last ', async () => { class Input1IsTen extends Validator { static get validatorName() { return 'Input1IsTen'; } execute(value) { const hasError = value.input1 !== 10; return hasError; } } const outSideButton = await fixture(html``); const el = await fixture(html` <${tag} .validators=${[new Input1IsTen()]}> <${childTag} name="input1" .validators=${[new IsNumber()]}> <${childTag} name="input2" .validators=${[new IsNumber()]}> `); const inputs = el.querySelectorAll(childTagString); inputs[1].modelValue = 2; // make it dirty inputs[1].focus(); outSideButton.focus(); await nextFrame(); expect(el.validationStates.error.Input1IsTen).to.be.true; expect(el.hasFeedbackFor).to.deep.equal(['error']); }); it('(re)initializes children interaction states on registration ready', async () => { const fieldset = await fixtureSync(html` <${tag} .modelValue="${{ a: '1', b: '2' }}"> <${childTag} name="a"> <${childTag} name="b"> `); const childA = fieldset.querySelector('[name="a"]'); const childB = fieldset.querySelector('[name="b"]'); const spyA = sinon.spy(childA, 'initInteractionState'); const spyB = sinon.spy(childB, 'initInteractionState'); expect(fieldset.prefilled).to.be.false; expect(fieldset.dirty).to.be.false; await fieldset.registrationReady; await nextFrame(); expect(spyA).to.have.been.called; expect(spyB).to.have.been.called; expect(fieldset.prefilled).to.be.true; expect(fieldset.dirty).to.be.false; }); }); // TODO: this should be tested in FormGroupMixin describe('serializedValue', () => { it('use form elements serializedValue', async () => { const fieldset = await fixture(html`<${tag}>${inputSlots}`); await nextFrame(); fieldset.formElements['hobbies[]'][0].serializer = v => `${v.value}-serialized`; fieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'Bar' }; fieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' }; fieldset.formElements['gender[]'][0].modelValue = { checked: false, value: 'male' }; fieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' }; fieldset.formElements.color.modelValue = { checked: false, value: 'blue' }; expect(fieldset.formElements['hobbies[]'][0].serializedValue).to.equal('Bar-serialized'); expect(fieldset.serializedValue).to.deep.equal({ 'hobbies[]': ['Bar-serialized', { checked: false, value: 'rugby' }], 'gender[]': [ { checked: false, value: 'male' }, { checked: false, value: 'female' }, ], color: { checked: false, value: 'blue' }, }); }); it('treats names with ending [] as arrays', async () => { const fieldset = await fixture(html`<${tag}>${inputSlots}`); await nextFrame(); fieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' }; fieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' }; fieldset.formElements['gender[]'][0].modelValue = { checked: false, value: 'male' }; fieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' }; fieldset.formElements.color.modelValue = { checked: false, value: 'blue' }; expect(fieldset.serializedValue).to.deep.equal({ 'hobbies[]': [ { checked: false, value: 'chess' }, { checked: false, value: 'rugby' }, ], 'gender[]': [ { checked: false, value: 'male' }, { checked: false, value: 'female' }, ], color: { checked: false, value: 'blue' }, }); }); it('0 is a valid value to be serialized', async () => { const fieldset = await fixture(html` <${tag}> <${childTag} name="price"> `); await nextFrame(); fieldset.formElements.price.modelValue = 0; expect(fieldset.serializedValue).to.deep.equal({ price: 0 }); }); it('serializes undefined values as ""(nb radios/checkboxes are always serialized)', async () => { const fieldset = await fixture(html` <${tag}> <${childTag} name="custom[]"> <${childTag} name="custom[]"> `); await nextFrame(); fieldset.formElements['custom[]'][0].modelValue = 'custom 1'; fieldset.formElements['custom[]'][1].modelValue = undefined; expect(fieldset.serializedValue).to.deep.equal({ 'custom[]': ['custom 1', ''], }); }); it('allows for nested fieldsets', async () => { const fieldset = await fixture(html` <${tag} name="userData"> <${childTag} name="comment"> <${tag} name="newfieldset">${inputSlots} `); await nextFrame(); const newFieldset = fieldset.querySelector('lion-fieldset'); newFieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' }; newFieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' }; newFieldset.formElements['gender[]'][0].modelValue = { checked: false, value: 'male' }; newFieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' }; newFieldset.formElements.color.modelValue = { checked: false, value: 'blue' }; fieldset.formElements.comment.modelValue = 'Foo'; expect(fieldset.formElements.keys().length).to.equal(2); expect(newFieldset.formElements.keys().length).to.equal(3); expect(fieldset.serializedValue).to.deep.equal({ comment: 'Foo', newfieldset: { 'hobbies[]': [ { checked: false, value: 'chess' }, { checked: false, value: 'rugby' }, ], 'gender[]': [ { checked: false, value: 'male' }, { checked: false, value: 'female' }, ], color: { checked: false, value: 'blue' }, }, }); }); it('does not serialize disabled values', async () => { const fieldset = await fixture(html` <${tag}> <${childTag} name="custom[]"> <${childTag} name="custom[]"> `); await nextFrame(); fieldset.formElements['custom[]'][0].modelValue = 'custom 1'; fieldset.formElements['custom[]'][1].disabled = true; expect(fieldset.serializedValue).to.deep.equal({ 'custom[]': ['custom 1'], }); }); it('will exclude form elements within a disabled fieldset', async () => { const fieldset = await fixture(html` <${tag} name="userData"> <${childTag} name="comment"> <${tag} name="newfieldset">${inputSlots} `); await nextFrame(); const newFieldset = fieldset.querySelector('lion-fieldset'); fieldset.formElements.comment.modelValue = 'Foo'; newFieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' }; newFieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' }; newFieldset.formElements['gender[]'][0].modelValue = { checked: false, value: 'male' }; newFieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' }; newFieldset.formElements.color.modelValue = { checked: false, value: 'blue' }; newFieldset.formElements.color.disabled = true; expect(fieldset.serializedValue).to.deep.equal({ comment: 'Foo', newfieldset: { 'hobbies[]': [ { checked: false, value: 'chess' }, { checked: false, value: 'rugby' }, ], 'gender[]': [ { checked: false, value: 'male' }, { checked: false, value: 'female' }, ], }, }); newFieldset.formElements.color.disabled = false; expect(fieldset.serializedValue).to.deep.equal({ comment: 'Foo', newfieldset: { 'hobbies[]': [ { checked: false, value: 'chess' }, { checked: false, value: 'rugby' }, ], 'gender[]': [ { checked: false, value: 'male' }, { checked: false, value: 'female' }, ], color: { checked: false, value: 'blue' }, }, }); }); }); describe('Reset', () => { it('restores default values if changes were made', async () => { const el = await fixture(html` <${tag}> <${childTag} id="firstName" name="firstName" .modelValue="${'Foo'}"> `); await el.querySelector(childTagString).updateComplete; const input = el.querySelector('#firstName'); input.modelValue = 'Bar'; expect(el.modelValue).to.deep.equal({ firstName: 'Bar' }); expect(input.modelValue).to.equal('Bar'); el.resetGroup(); expect(el.modelValue).to.deep.equal({ firstName: 'Foo' }); expect(input.modelValue).to.equal('Foo'); }); it('restores default values of arrays if changes were made', async () => { const el = await fixture(html` <${tag}> <${childTag} id="firstName" name="firstName[]" .modelValue="${'Foo'}"> `); await el.querySelector(childTagString).updateComplete; const input = el.querySelector('#firstName'); input.modelValue = 'Bar'; expect(el.modelValue).to.deep.equal({ 'firstName[]': ['Bar'] }); expect(input.modelValue).to.equal('Bar'); el.resetGroup(); expect(el.modelValue).to.deep.equal({ 'firstName[]': ['Foo'] }); expect(input.modelValue).to.equal('Foo'); }); it('restores default values of a nested fieldset if changes were made', async () => { const el = await fixture(html` <${tag}> <${tag} id="name" name="name[]"> <${childTag} id="firstName" name="firstName" .modelValue="${'Foo'}"> `); await Promise.all([ el.querySelector('lion-fieldset').updateComplete, el.querySelector(childTagString).updateComplete, ]); const input = el.querySelector('#firstName'); const nestedFieldset = el.querySelector('#name'); input.modelValue = 'Bar'; expect(el.modelValue).to.deep.equal({ 'name[]': [{ firstName: 'Bar' }] }); expect(nestedFieldset.modelValue).to.deep.equal({ firstName: 'Bar' }); expect(input.modelValue).to.equal('Bar'); el.resetGroup(); expect(el.modelValue).to.deep.equal({ 'name[]': [{ firstName: 'Foo' }] }); expect(nestedFieldset.modelValue).to.deep.equal({ firstName: 'Foo' }); expect(input.modelValue).to.equal('Foo'); }); it('clears interaction state', async () => { const el = await fixture(html`<${tag} touched dirty>${inputSlots}`); await nextFrame(); // Safety check initially el._setValueForAllFormElements('prefilled', true); expect(el.dirty).to.equal(true, '"dirty" initially'); expect(el.touched).to.equal(true, '"touched" initially'); expect(el.prefilled).to.equal(true, '"prefilled" initially'); // Reset all children states, with prefilled false el._setValueForAllFormElements('modelValue', {}); el.resetInteractionState(); expect(el.dirty).to.equal(false, 'not "dirty" after reset'); expect(el.touched).to.equal(false, 'not "touched" after reset'); expect(el.prefilled).to.equal(false, 'not "prefilled" after reset'); // Reset all children states with prefilled true el._setValueForAllFormElements('modelValue', { checked: true }); // not prefilled el.resetInteractionState(); expect(el.dirty).to.equal(false, 'not "dirty" after 2nd reset'); expect(el.touched).to.equal(false, 'not "touched" after 2nd reset'); // prefilled state is dependant on value expect(el.prefilled).to.equal(true, '"prefilled" after 2nd reset'); }); it('clears submitted state', async () => { const fieldset = await fixture(html`<${tag}>${inputSlots}`); await nextFrame(); fieldset.submitted = true; fieldset.resetGroup(); expect(fieldset.submitted).to.equal(false); fieldset.formElements.forEach(el => { expect(el.submitted).to.equal(false); }); }); it('has correct validation afterwards', async () => { class IsCat extends Validator { static get validatorName() { return 'IsCat'; } execute(value) { const hasError = value !== 'cat'; return hasError; } } class ColorContainsA extends Validator { static get validatorName() { return 'ColorContainsA'; } execute(value) { const hasError = value.color.indexOf('a') === -1; return hasError; } } const el = await fixture(html` <${tag} .validators=${[new ColorContainsA()]}> <${childTag} name="color" .validators=${[new IsCat()]}> <${childTag} name="color2"> `); await el.registrationReady; expect(el.hasFeedbackFor).to.deep.equal(['error']); expect(el.validationStates.error.ColorContainsA).to.be.true; expect(el.formElements.color.hasFeedbackFor).to.deep.equal([]); el.formElements.color.modelValue = 'onlyb'; expect(el.hasFeedbackFor).to.deep.equal(['error']); expect(el.validationStates.error.ColorContainsA).to.be.true; expect(el.formElements.color.validationStates.error.IsCat).to.be.true; el.formElements.color.modelValue = 'cat'; expect(el.hasFeedbackFor).to.deep.equal([]); el.resetGroup(); expect(el.hasFeedbackFor).to.deep.equal(['error']); expect(el.validationStates.error.ColorContainsA).to.be.true; expect(el.formElements.color.hasFeedbackFor).to.deep.equal([]); }); it('has access to `_initialModelValue` based on initial children states', async () => { const el = await fixture(html` <${tag}> <${childTag} name="child[]" .modelValue="${'foo1'}"> <${childTag} name="child[]" .modelValue="${'bar1'}"> `); await el.updateComplete; el.modelValue['child[]'] = ['foo2', 'bar2']; expect(el._initialModelValue['child[]']).to.eql(['foo1', 'bar1']); }); it('does not wrongly recompute `_initialModelValue` after dynamic changes of children', async () => { const el = await fixture(html` <${tag}> <${childTag} name="child[]" .modelValue="${'foo1'}"> `); el.modelValue['child[]'] = ['foo2']; const childEl = await fixture(html` <${childTag} name="child[]" .modelValue="${'bar1'}"> `); el.appendChild(childEl); expect(el._initialModelValue['child[]']).to.eql(['foo1', 'bar1']); }); describe('resetGroup method', () => { it('calls resetGroup on children fieldsets', async () => { const el = await fixture(html` <${tag} name="parentFieldset"> <${tag} name="childFieldset"> <${childTag} name="child[]" .modelValue="${'foo1'}"> `); const childFieldsetEl = el.querySelector(tagString); const resetGroupSpy = sinon.spy(childFieldsetEl, 'resetGroup'); el.resetGroup(); expect(resetGroupSpy.callCount).to.equal(1); }); it('calls reset on children fields', async () => { const el = await fixture(html` <${tag} name="parentFieldset"> <${tag} name="childFieldset"> <${childTag} name="child[]" .modelValue="${'foo1'}"> `); const childFieldsetEl = el.querySelector(childTagString); const resetSpy = sinon.spy(childFieldsetEl, 'reset'); el.resetGroup(); expect(resetSpy.callCount).to.equal(1); }); }); describe('clearGroup method', () => { it('calls clearGroup on children fieldset', async () => { const el = await fixture(html` <${tag} name="parentFieldset"> <${tag} name="childFieldset"> <${childTag} name="child[]" .modelValue="${'foo1'}"> `); const childFieldsetEl = el.querySelector(tagString); const clearGroupSpy = sinon.spy(childFieldsetEl, 'clearGroup'); el.clearGroup(); expect(clearGroupSpy.callCount).to.equal(1); }); it('calls clear on children fields', async () => { const el = await fixture(html` <${tag} name="parentFieldset"> <${tag} name="childFieldset"> <${childTag} name="child[]" .modelValue="${'foo1'}"> `); const childFieldsetEl = el.querySelector(childTagString); const clearSpy = sinon.spy(childFieldsetEl, 'clear'); el.clearGroup(); expect(clearSpy.callCount).to.equal(1); }); it('should clear the value of fields', async () => { const el = await fixture(html` <${tag} name="parentFieldset"> <${tag} name="childFieldset"> <${childTag} name="child" .modelValue="${'foo1'}"> `); el.clearGroup(); expect(el.querySelector('[name="child"]').modelValue).to.equal(''); }); }); }); describe('Accessibility', () => { it('has role="group" set', async () => { const fieldset = await fixture(html`<${tag}>${inputSlots}`); await nextFrame(); fieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' }; fieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' }; fieldset.formElements['gender[]'][0].modelValue = { checked: false, value: 'male' }; fieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' }; fieldset.formElements.color.modelValue = { checked: false, value: 'blue' }; expect(fieldset.hasAttribute('role')).to.be.true; expect(fieldset.getAttribute('role')).to.contain('group'); }); it('has an aria-labelledby from element with slot="label"', async () => { const el = await fixture(html` <${tag}> ${inputSlots} `); const label = Array.from(el.children).find(child => child.slot === 'label'); expect(el.hasAttribute('aria-labelledby')).to.equal(true); expect(el.getAttribute('aria-labelledby')).contains(label.id); }); describe('Screen reader relations (aria-describedby) for child fields and fieldsets', () => { let childAriaFixture; // function let childAriaTest; // function before(() => { // Legend: // - l1 means level 1 (outer) fieldset // - l2 means level 2 (inner) fieldset // - g means group: the help-text or feedback belongs to group // - f means field(lion-input in fixture below): the help-text or feedback belongs to field // - 'a' or 'b' behind 'f' indicate which field in a fieldset is meant (a: first, b: second) childAriaFixture = async ( msgSlotType = 'feedback', // eslint-disable-line no-shadow ) => { const dom = await fixture(html` <${tag} name="l1_g"> <${childTag} name="l1_fa">
<${childTag} name="l1_fb">
<${tag} name="l2_g"> <${childTag} name="l2_fa">
<${childTag} name="l2_fb">
`); return dom; }; // eslint-disable-next-line no-shadow childAriaTest = childAriaFixture => { /* eslint-disable camelcase */ // Message elements: all elements pointed at by inputs const msg_l1_g = childAriaFixture.querySelector('#msg_l1_g'); const msg_l1_fa = childAriaFixture.querySelector('#msg_l1_fa'); const msg_l1_fb = childAriaFixture.querySelector('#msg_l1_fb'); const msg_l2_g = childAriaFixture.querySelector('#msg_l2_g'); const msg_l2_fa = childAriaFixture.querySelector('#msg_l2_fa'); const msg_l2_fb = childAriaFixture.querySelector('#msg_l2_fb'); // Field elements: all inputs pointing to message elements const input_l1_fa = childAriaFixture.querySelector('input[name=l1_fa]'); const input_l1_fb = childAriaFixture.querySelector('input[name=l1_fb]'); const input_l2_fa = childAriaFixture.querySelector('input[name=l2_fa]'); const input_l2_fb = childAriaFixture.querySelector('input[name=l2_fb]'); /* eslint-enable camelcase */ // 'L1' fields (inside lion-fieldset[name="l1_g"]) should point to l1(group) msg expect(input_l1_fa.getAttribute('aria-describedby')).to.contain( msg_l1_g.id, 'l1 input(a) refers parent/group', ); expect(input_l1_fb.getAttribute('aria-describedby')).to.contain( msg_l1_g.id, 'l1 input(b) refers parent/group', ); // Also check that aria-describedby of the inputs are not overridden (this relation was // put there in lion-input(using lion-field)). expect(input_l1_fa.getAttribute('aria-describedby')).to.contain( msg_l1_fa.id, 'l1 input(a) refers local field', ); expect(input_l1_fb.getAttribute('aria-describedby')).to.contain( msg_l1_fb.id, 'l1 input(b) refers local field', ); // Also make feedback element point to nested fieldset inputs expect(input_l2_fa.getAttribute('aria-describedby')).to.contain( msg_l1_g.id, 'l2 input(a) refers grandparent/group.group', ); expect(input_l2_fb.getAttribute('aria-describedby')).to.contain( msg_l1_g.id, 'l2 input(b) refers grandparent/group.group', ); // Check order: the nearest ('dom wise': so 1. local, 2. parent, 3. grandparent) message // should be read first by screen reader const dA = input_l2_fa.getAttribute('aria-describedby'); expect( dA.indexOf(msg_l2_fa.id) < dA.indexOf(msg_l2_g.id) < dA.indexOf(msg_l1_g.id), ).to.equal(true, 'order of ids'); const dB = input_l2_fb.getAttribute('aria-describedby'); expect( dB.indexOf(msg_l2_fb.id) < dB.indexOf(msg_l2_g.id) < dB.indexOf(msg_l1_g.id), ).to.equal(true, 'order of ids'); }; }); it(`reads feedback message belonging to fieldset when child input is focused (via aria-describedby)`, async () => { childAriaTest(await childAriaFixture('feedback')); }); it(`reads help-text message belonging to fieldset when child input is focused (via aria-describedby)`, async () => { childAriaTest(await childAriaFixture('help-text')); }); }); }); });