From 8f1456c8cb6c341454b476047c8ff1867bf84b56 Mon Sep 17 00:00:00 2001 From: qa46hx Date: Wed, 28 Oct 2020 17:09:49 +0100 Subject: [PATCH] chore(form-core): make use of ChoiceInput & ChoiceGroup test suites --- .../lion-checkbox-group-integrations.test.js | 9 + .../test/lion-checkbox-group.test.js | 45 +++ .../test/lion-checkbox-integrations.test.js | 4 + .../checkbox-group/test/lion-checkbox.test.js | 39 +++ .../choice-group/ChoiceGroupMixin.suite.js | 224 ++++++++------ .../choice-group/ChoiceInputMixin.suite.js | 285 ++++++++++++++++++ .../choice-group/ChoiceInputMixin.test.js | 277 +---------------- .../lion-radio-group-integrations.test.js | 9 + .../radio-group/test/lion-radio-group.test.js | 40 +++ .../test/lion-radio-integrations.test.js | 4 + packages/radio-group/test/lion-radio.test.js | 34 ++- 11 files changed, 603 insertions(+), 367 deletions(-) create mode 100644 packages/checkbox-group/test/lion-checkbox-group-integrations.test.js create mode 100644 packages/checkbox-group/test/lion-checkbox-integrations.test.js create mode 100644 packages/checkbox-group/test/lion-checkbox.test.js create mode 100644 packages/form-core/test-suites/choice-group/ChoiceInputMixin.suite.js create mode 100644 packages/radio-group/test/lion-radio-group-integrations.test.js create mode 100644 packages/radio-group/test/lion-radio-integrations.test.js diff --git a/packages/checkbox-group/test/lion-checkbox-group-integrations.test.js b/packages/checkbox-group/test/lion-checkbox-group-integrations.test.js new file mode 100644 index 000000000..2950f10a4 --- /dev/null +++ b/packages/checkbox-group/test/lion-checkbox-group-integrations.test.js @@ -0,0 +1,9 @@ +import { runChoiceGroupMixinSuite } from '@lion/form-core/test-suites/choice-group/ChoiceGroupMixin.suite.js'; +import '../lion-checkbox-group.js'; +import '../lion-checkbox.js'; + +runChoiceGroupMixinSuite({ + parentTagString: 'lion-checkbox-group', + childTagString: 'lion-checkbox', + choiceType: 'multiple', +}); diff --git a/packages/checkbox-group/test/lion-checkbox-group.test.js b/packages/checkbox-group/test/lion-checkbox-group.test.js index 932f1ad04..408dffb06 100644 --- a/packages/checkbox-group/test/lion-checkbox-group.test.js +++ b/packages/checkbox-group/test/lion-checkbox-group.test.js @@ -15,6 +15,51 @@ beforeEach(() => { }); describe('', () => { + describe('resetGroup', () => { + // TODO move to FormGroupMixin test suite and let CheckboxGroup make use of them + it('restores default values of arrays if changes were made', async () => { + const el = await fixture(html` + + + + + + `); + el.formElements[0].checked = true; + expect(el.modelValue).to.deep.equal(['Archimedes']); + + el.resetGroup(); + expect(el.modelValue).to.deep.equal([]); + }); + + it('restores default values of arrays if changes were made', async () => { + const el = await fixture(html` + + + + + + `); + el.formElements[0].checked = true; + expect(el.modelValue).to.deep.equal(['Archimedes', 'Francis Bacon']); + + el.resetGroup(); + expect(el.modelValue).to.deep.equal(['Francis Bacon']); + + el.formElements[2].checked = true; + expect(el.modelValue).to.deep.equal(['Francis Bacon', 'Marie Curie']); + + el.resetGroup(); + expect(el.modelValue).to.deep.equal(['Francis Bacon']); + }); + }); + it('is accessible', async () => { const el = await fixture(html` diff --git a/packages/checkbox-group/test/lion-checkbox-integrations.test.js b/packages/checkbox-group/test/lion-checkbox-integrations.test.js new file mode 100644 index 000000000..bfd5d897e --- /dev/null +++ b/packages/checkbox-group/test/lion-checkbox-integrations.test.js @@ -0,0 +1,4 @@ +import '../lion-checkbox.js'; +import { runChoiceInputMixinSuite } from '@lion/form-core/test-suites/choice-group/ChoiceInputMixin.suite.js'; + +runChoiceInputMixinSuite({ tagString: 'lion-checkbox' }); diff --git a/packages/checkbox-group/test/lion-checkbox.test.js b/packages/checkbox-group/test/lion-checkbox.test.js new file mode 100644 index 000000000..3726c618d --- /dev/null +++ b/packages/checkbox-group/test/lion-checkbox.test.js @@ -0,0 +1,39 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import '../lion-checkbox.js'; + +/** + * @typedef {import('../src/LionCheckbox').LionCheckbox} LionCheckbox + */ + +describe('', () => { + it('should have type = checkbox', async () => { + const el = await fixture(html` + + `); + expect(el.getAttribute('type')).to.equal('checkbox'); + }); + + it('can be reset when unchecked by default', async () => { + const el = /** @type {LionCheckbox} */ (await fixture(html` + + `)); + expect(el._initialModelValue).to.deep.equal({ value: 'male', checked: false }); + el.checked = true; + expect(el.modelValue).to.deep.equal({ value: 'male', checked: true }); + + el.reset(); + expect(el.modelValue).to.deep.equal({ value: 'male', checked: false }); + }); + + it('can be reset when checked by default', async () => { + const el = /** @type {LionCheckbox} */ (await fixture(html` + + `)); + expect(el._initialModelValue).to.deep.equal({ value: 'male', checked: true }); + el.checked = false; + expect(el.modelValue).to.deep.equal({ value: 'male', checked: false }); + + el.reset(); + expect(el.modelValue).to.deep.equal({ value: 'male', checked: true }); + }); +}); diff --git a/packages/form-core/test-suites/choice-group/ChoiceGroupMixin.suite.js b/packages/form-core/test-suites/choice-group/ChoiceGroupMixin.suite.js index d250f7b5f..2640f68ea 100644 --- a/packages/form-core/test-suites/choice-group/ChoiceGroupMixin.suite.js +++ b/packages/form-core/test-suites/choice-group/ChoiceGroupMixin.suite.js @@ -13,51 +13,54 @@ class ChoiceGroup extends ChoiceGroupMixin(FormGroupMixin(LitElement)) {} customElements.define('choice-group', ChoiceGroup); /** - * @param {{ parentTagString?:string, childTagString?: string}} [config] + * @param {{ parentTagString?:string, childTagString?: string, choiceType?: string}} [config] */ -export function runChoiceGroupMixinSuite({ parentTagString, childTagString } = {}) { +export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choiceType } = {}) { const cfg = { parentTagString: parentTagString || 'choice-group', childTagString: childTagString || 'choice-group-input', + choiceType: choiceType || 'single', }; const parentTag = unsafeStatic(cfg.parentTagString); const childTag = unsafeStatic(cfg.childTagString); - describe('ChoiceGroupMixin', () => { - it('has a single modelValue representing the currently checked radio value', async () => { - const el = /** @type {ChoiceGroup} */ (await fixture(html` - <${parentTag} name="gender"> - <${childTag} .choiceValue=${'male'}> - <${childTag} .choiceValue=${'female'} checked> - <${childTag} .choiceValue=${'other'}> - - `)); - expect(el.modelValue).to.equal('female'); - el.formElements[0].checked = true; - expect(el.modelValue).to.equal('male'); - el.formElements[2].checked = true; - expect(el.modelValue).to.equal('other'); - }); + describe(`ChoiceGroupMixin: ${cfg.parentTagString}`, () => { + if (cfg.choiceType === 'single') { + it('has a single modelValue representing the currently checked radio value', async () => { + const el = /** @type {ChoiceGroup} */ (await fixture(html` + <${parentTag} name="gender[]"> + <${childTag} .choiceValue=${'male'}> + <${childTag} .choiceValue=${'female'} checked> + <${childTag} .choiceValue=${'other'}> + + `)); + expect(el.modelValue).to.equal('female'); + el.formElements[0].checked = true; + expect(el.modelValue).to.equal('male'); + el.formElements[2].checked = true; + expect(el.modelValue).to.equal('other'); + }); - it('has a single formattedValue representing the currently checked radio value', async () => { - const el = /** @type {ChoiceGroup} */ (await fixture(html` - <${parentTag} name="gender"> - <${childTag} .choiceValue=${'male'}> - <${childTag} .choiceValue=${'female'} checked> - <${childTag} .choiceValue=${'other'}> - - `)); - expect(el.formattedValue).to.equal('female'); - el.formElements[0].checked = true; - expect(el.formattedValue).to.equal('male'); - el.formElements[2].checked = true; - expect(el.formattedValue).to.equal('other'); - }); + it('has a single formattedValue representing the currently checked radio value', async () => { + const el = /** @type {ChoiceGroup} */ (await fixture(html` + <${parentTag} name="gender"> + <${childTag} .choiceValue=${'male'}> + <${childTag} .choiceValue=${'female'} checked> + <${childTag} .choiceValue=${'other'}> + + `)); + expect(el.formattedValue).to.equal('female'); + el.formElements[0].checked = true; + expect(el.formattedValue).to.equal('male'); + el.formElements[2].checked = true; + expect(el.formattedValue).to.equal('other'); + }); + } it('throws if a child element without a modelValue like { value: "foo", checked: false } tries to register', async () => { const el = /** @type {ChoiceGroup} */ (await fixture(html` - <${parentTag} name="gender"> + <${parentTag} name="gender[]"> <${childTag} .choiceValue=${'male'}> <${childTag} .choiceValue=${'female'} checked> <${childTag} .choiceValue=${'other'}> @@ -70,68 +73,68 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString } = { expect(() => { el.addFormElement(invalidChild); }).to.throw( - 'The choice-group name="gender" does not allow to register choice-group-input with .modelValue="Lara" - The modelValue should represent an Object { value: "foo", checked: false }', + `The ${cfg.parentTagString} name="gender[]" does not allow to register ${cfg.childTagString} with .modelValue="Lara" - The modelValue should represent an Object { value: "foo", checked: false }`, ); }); it('automatically sets the name property of child fields to its own name', async () => { const el = /** @type {ChoiceGroup} */ (await fixture(html` - <${parentTag} name="gender"> + <${parentTag} name="gender[]"> <${childTag} .choiceValue=${'female'} checked> <${childTag} .choiceValue=${'other'}> `)); - expect(el.formElements[0].name).to.equal('gender'); - expect(el.formElements[1].name).to.equal('gender'); + expect(el.formElements[0].name).to.equal('gender[]'); + expect(el.formElements[1].name).to.equal('gender[]'); const validChild = /** @type {ChoiceGroup} */ (await fixture(html` <${childTag} .choiceValue=${'male'}> `)); el.appendChild(validChild); - expect(el.formElements[2].name).to.equal('gender'); + expect(el.formElements[2].name).to.equal('gender[]'); }); it('automatically updates the name property of child fields to its own name', async () => { const el = /** @type {ChoiceGroup} */ (await fixture(html` - <${parentTag} name="gender"> + <${parentTag} name="gender[]"> <${childTag}> <${childTag}> `)); - expect(el.formElements[0].name).to.equal('gender'); - expect(el.formElements[1].name).to.equal('gender'); + expect(el.formElements[0].name).to.equal('gender[]'); + expect(el.formElements[1].name).to.equal('gender[]'); - el.name = 'gender2'; + el.name = 'gender2[]'; await el.updateComplete; - expect(el.formElements[0].name).to.equal('gender2'); - expect(el.formElements[1].name).to.equal('gender2'); + expect(el.formElements[0].name).to.equal('gender2[]'); + expect(el.formElements[1].name).to.equal('gender2[]'); }); it('prevents updating the name property of a child if it is different from its parent', async () => { const el = /** @type {ChoiceGroup} */ (await fixture(html` - <${parentTag} name="gender"> + <${parentTag} name="gender[]"> <${childTag}> <${childTag}> `)); - expect(el.formElements[0].name).to.equal('gender'); - expect(el.formElements[1].name).to.equal('gender'); + expect(el.formElements[0].name).to.equal('gender[]'); + expect(el.formElements[1].name).to.equal('gender[]'); - el.formElements[0].name = 'gender2'; + el.formElements[0].name = 'gender2[]'; await el.formElements[0].updateComplete; - expect(el.formElements[0].name).to.equal('gender'); + expect(el.formElements[0].name).to.equal('gender[]'); }); it('adjusts the name of a child element if it has a different name than the group', async () => { const el = /** @type {ChoiceGroup} */ (await fixture(html` - <${parentTag} name="gender"> + <${parentTag} name="gender[]"> <${childTag} .choiceValue=${'female'} checked> <${childTag} .choiceValue=${'other'}> @@ -142,45 +145,57 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString } = { `)); el.addFormElement(invalidChild); await invalidChild.updateComplete; - expect(invalidChild.name).to.equal('gender'); + expect(invalidChild.name).to.equal('gender[]'); }); it('can set initial modelValue on creation', async () => { const el = /** @type {ChoiceGroup} */ (await fixture(html` - <${parentTag} name="gender" .modelValue=${'other'}> + <${parentTag} name="gender[]" .modelValue=${'other'}> <${childTag} .choiceValue=${'male'}> <${childTag} .choiceValue=${'female'}> <${childTag} .choiceValue=${'other'}> `)); - expect(el.modelValue).to.equal('other'); + if (cfg.choiceType === 'single') { + expect(el.modelValue).to.equal('other'); + } else { + expect(el.modelValue).to.deep.equal(['other']); + } expect(el.formElements[2].checked).to.be.true; }); it('can set initial serializedValue on creation', async () => { const el = /** @type {ChoiceGroup} */ (await fixture(html` - <${parentTag} name="gender" .serializedValue=${'other'}> + <${parentTag} name="gender[]" .serializedValue=${'other'}> <${childTag} .choiceValue=${'male'}> <${childTag} .choiceValue=${'female'}> <${childTag} .choiceValue=${'other'}> `)); - expect(el.serializedValue).to.equal('other'); + if (cfg.choiceType === 'single') { + expect(el.serializedValue).to.equal('other'); + } else { + expect(el.serializedValue).to.deep.equal(['other']); + } expect(el.formElements[2].checked).to.be.true; }); it('can set initial formattedValue on creation', async () => { const el = /** @type {ChoiceGroup} */ (await fixture(html` - <${parentTag} name="gender" .formattedValue=${'other'}> + <${parentTag} name="gender[]" .formattedValue=${'other'}> <${childTag} .choiceValue=${'male'}> <${childTag} .choiceValue=${'female'}> <${childTag} .choiceValue=${'other'}> `)); - expect(el.formattedValue).to.equal('other'); + if (cfg.choiceType === 'single') { + expect(el.formattedValue).to.equal('other'); + } else { + expect(el.formattedValue).to.deep.equal(['other']); + } expect(el.formElements[2].checked).to.be.true; }); @@ -188,33 +203,45 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString } = { const date = new Date(2018, 11, 24, 10, 33, 30, 0); const el = /** @type {ChoiceGroup} */ (await fixture(html` - <${parentTag} name="data"> + <${parentTag} name="data[]"> <${childTag} .choiceValue=${{ some: 'data' }}> <${childTag} .choiceValue=${date} checked> `)); - expect(el.modelValue).to.equal(date); - el.formElements[0].checked = true; - expect(el.modelValue).to.deep.equal({ some: 'data' }); + if (cfg.choiceType === 'single') { + expect(el.modelValue).to.equal(date); + el.formElements[0].checked = true; + expect(el.modelValue).to.deep.equal({ some: 'data' }); + } else { + expect(el.modelValue).to.deep.equal([date]); + el.formElements[0].checked = true; + expect(el.modelValue).to.deep.equal([{ some: 'data' }, date]); + } }); it('can handle 0 and empty string as valid values', async () => { const el = /** @type {ChoiceGroup} */ (await fixture(html` - <${parentTag} name="data"> + <${parentTag} name="data[]"> <${childTag} .choiceValue=${0} checked> <${childTag} .choiceValue=${''}> `)); - expect(el.modelValue).to.equal(0); - el.formElements[1].checked = true; - expect(el.modelValue).to.equal(''); + if (cfg.choiceType === 'single') { + expect(el.modelValue).to.equal(0); + el.formElements[1].checked = true; + expect(el.modelValue).to.equal(''); + } else { + expect(el.modelValue).to.deep.equal([0]); + el.formElements[1].checked = true; + expect(el.modelValue).to.deep.equal([0, '']); + } }); it('can check a radio by supplying an available modelValue', async () => { const el = /** @type {ChoiceGroup} */ (await fixture(html` - <${parentTag} name="gender"> + <${parentTag} name="gender[]"> <${childTag} .modelValue="${{ value: 'male', checked: false }}" > @@ -227,7 +254,11 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString } = { `)); - expect(el.modelValue).to.equal('female'); + if (cfg.choiceType === 'single') { + expect(el.modelValue).to.equal('female'); + } else { + expect(el.modelValue).to.deep.equal(['female']); + } el.modelValue = 'other'; expect(el.formElements[2].checked).to.be.true; }); @@ -236,7 +267,7 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString } = { let counter = 0; const el = /** @type {ChoiceGroup} */ (await fixture(html` <${parentTag} - name="gender" + name="gender[]" @model-value-changed=${() => { counter += 1; }} @@ -252,26 +283,37 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString } = { counter = 0; // reset after setup which may result in different results el.formElements[0].checked = true; - expect(counter).to.equal(1); // male becomes checked, female becomes unchecked + expect(counter).to.equal(1); // male becomes checked // not changed values trigger no event el.formElements[0].checked = true; expect(counter).to.equal(1); el.formElements[2].checked = true; - expect(counter).to.equal(2); // other becomes checked, male becomes unchecked + expect(counter).to.equal(2); // other becomes checked - // not found values trigger no event - el.modelValue = 'foo'; - expect(counter).to.equal(2); + if (cfg.choiceType === 'single') { + // not found values trigger no event + el.modelValue = 'foo'; + expect(counter).to.equal(2); - el.modelValue = 'male'; - expect(counter).to.equal(3); // male becomes checked, other becomes unchecked + el.modelValue = 'male'; + expect(counter).to.equal(3); // male becomes checked, other becomes unchecked + } + + if (choiceType === 'multiple') { + // not found values trigger no event + el.modelValue = ['foo', 'male', 'female', 'other']; + expect(counter).to.equal(2); + + el.modelValue = ['female', 'other']; + expect(counter).to.equal(3); // male becomes unchecked + } }); it('can be required', async () => { const el = /** @type {ChoiceGroup} */ (await fixture(html` - <${parentTag} name="gender" .validators=${[new Required()]}> + <${parentTag} name="gender[]" .validators=${[new Required()]}> <${childTag} .choiceValue=${'male'}> <${childTag} .choiceValue=${{ subObject: 'satisfies required' }} @@ -295,24 +337,32 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString } = { it('returns serialized value', async () => { const el = /** @type {ChoiceGroup} */ (await fixture(html` - <${parentTag} name="gender"> + <${parentTag} name="gender[]"> <${childTag} .choiceValue=${'male'}> <${childTag} .choiceValue=${'female'}> `)); el.formElements[0].checked = true; - expect(el.serializedValue).to.deep.equal('male'); + if (cfg.choiceType === 'single') { + expect(el.serializedValue).to.deep.equal('male'); + } else { + expect(el.serializedValue).to.deep.equal(['male']); + } }); it('returns serialized value on unchecked state', async () => { const el = /** @type {ChoiceGroup} */ (await fixture(html` - <${parentTag} name="gender"> + <${parentTag} name="gender[]"> <${childTag} .choiceValue=${'male'}> <${childTag} .choiceValue=${'female'}> `)); - expect(el.serializedValue).to.deep.equal(''); + if (cfg.choiceType === 'single') { + expect(el.serializedValue).to.deep.equal(''); + } else { + expect(el.serializedValue).to.deep.equal([]); + } }); describe('multipleChoice', () => { @@ -403,7 +453,7 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString } = { it('will serialize all children with their serializedValue', async () => { const el = /** @type {ChoiceGroup} */ (await fixture(html` - <${parentTag} name="gender"> + <${parentTag} name="gender[]"> <${childTag} .choiceValue=${'male'} checked disabled> <${childTag} .choiceValue=${'female'} checked> <${childTag} .choiceValue=${'other'}> @@ -411,15 +461,11 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString } = { `)); - expect(el.serializedValue).to.eql({ - gender: 'female', - }); - - const choiceGroupEl = /** @type {ChoiceGroup} */ (el.querySelector('[name="gender"]')); - choiceGroupEl.multipleChoice = true; - expect(el.serializedValue).to.eql({ - gender: ['female'], - }); + if (cfg.choiceType === 'single') { + expect(el.serializedValue).to.deep.equal({ 'gender[]': ['female'] }); + } else { + expect(el.serializedValue).to.deep.equal({ 'gender[]': [['female']] }); + } }); }); }); diff --git a/packages/form-core/test-suites/choice-group/ChoiceInputMixin.suite.js b/packages/form-core/test-suites/choice-group/ChoiceInputMixin.suite.js new file mode 100644 index 000000000..1b96336ee --- /dev/null +++ b/packages/form-core/test-suites/choice-group/ChoiceInputMixin.suite.js @@ -0,0 +1,285 @@ +import { Required } from '@lion/form-core'; +import { LionInput } from '@lion/input'; +import { expect, fixture, html, unsafeStatic } from '@open-wc/testing'; +import sinon from 'sinon'; +import { ChoiceInputMixin } from '../../src/choice-group/ChoiceInputMixin.js'; + +class ChoiceInput extends ChoiceInputMixin(LionInput) { + constructor() { + super(); + this.type = 'checkbox'; + } +} +customElements.define('choice-group-input', ChoiceInput); + +/** + * @param {{ tagString?:string, tagType?: string}} [config] + */ +export function runChoiceInputMixinSuite({ tagString } = {}) { + const cfg = { + tagString: tagString || 'choice-group-input', + }; + + const tag = unsafeStatic(cfg.tagString); + describe(`ChoiceInputMixin: ${tagString}`, () => { + it('is hidden when attribute hidden is true', async () => { + const el = /** @type {ChoiceInput} */ (await fixture(html`<${tag} hidden>`)); + expect(el).not.to.be.displayed; + }); + + it('has choiceValue', async () => { + const el = /** @type {ChoiceInput} */ (await fixture( + html`<${tag} .choiceValue=${'foo'}>`, + )); + + expect(el.choiceValue).to.equal('foo'); + expect(el.modelValue).to.deep.equal({ + value: 'foo', + checked: false, + }); + }); + + it('can handle complex data via choiceValue', async () => { + const date = new Date(2018, 11, 24, 10, 33, 30, 0); + + const el = /** @type {ChoiceInput} */ (await fixture( + html`<${tag} .choiceValue=${date}>`, + )); + + expect(el.choiceValue).to.equal(date); + expect(el.modelValue.value).to.equal(date); + }); + + it('fires one "model-value-changed" event if choiceValue or checked state or modelValue changed', async () => { + let counter = 0; + const el = /** @type {ChoiceInput} */ (await fixture(html` + <${tag} + @model-value-changed=${() => { + counter += 1; + }} + .choiceValue=${'foo'} + > + `)); + expect(counter).to.equal(1); // undefined to set value + + el.checked = true; + expect(counter).to.equal(2); + + // no change means no event + el.checked = true; + el.choiceValue = 'foo'; + el.modelValue = { value: 'foo', checked: true }; + expect(counter).to.equal(2); + + el.modelValue = { value: 'foo', checked: false }; + expect(counter).to.equal(3); + }); + + it('fires one "user-input-changed" event after user interaction', async () => { + let counter = 0; + const el = /** @type {ChoiceInput} */ (await fixture(html` + <${tag} + @user-input-changed="${() => { + counter += 1; + }}" + > + + + `)); + expect(counter).to.equal(0); + // Here we try to mimic user interaction by firing browser events + const nativeInput = el._inputNode; + nativeInput.dispatchEvent(new CustomEvent('input', { bubbles: true })); // fired by (at least) Chrome + expect(counter).to.equal(0); + nativeInput.dispatchEvent(new CustomEvent('change', { bubbles: true })); + expect(counter).to.equal(1); + }); + + it('can be required', async () => { + const el = /** @type {ChoiceInput} */ (await fixture(html` + <${tag} .choiceValue=${'foo'} .validators=${[new Required()]}> + `)); + expect(el.hasFeedbackFor).to.include('error'); + expect(el.validationStates.error).to.exist; + expect(el.validationStates.error.Required).to.exist; + el.checked = true; + expect(el.hasFeedbackFor).not.to.include('error'); + expect(el.validationStates.error).to.exist; + expect(el.validationStates.error.Required).not.to.exist; + }); + + describe('Checked state synchronization', () => { + it('synchronizes checked state initially (via attribute or property)', async () => { + const el = /** @type {ChoiceInput} */ (await fixture(html`<${tag}>`)); + expect(el.checked).to.equal(false, 'initially unchecked'); + + const precheckedElementAttr = /** @type {ChoiceInput} */ (await fixture(html` + <${tag} .checked=${true}> + `)); + expect(precheckedElementAttr.checked).to.equal(true, 'initially checked via attribute'); + }); + + it('can be checked and unchecked programmatically', async () => { + const el = /** @type {ChoiceInput} */ (await fixture(html`<${tag}>`)); + expect(el.checked).to.be.false; + el.checked = true; + expect(el.checked).to.be.true; + + await el.updateComplete; + expect(el._inputNode.checked).to.be.true; + }); + + it('can be checked and unchecked via user interaction', async () => { + const el = /** @type {ChoiceInput} */ (await fixture(html`<${tag}>`)); + el._inputNode.click(); + expect(el.checked).to.be.true; + el._inputNode.click(); + await el.updateComplete; + if (el.type === 'checkbox') { + expect(el.checked).to.be.false; + } + }); + + it('can not toggle the checked state when disabled via user interaction', async () => { + const el = /** @type {ChoiceInput} */ (await fixture(html`<${tag} disabled>`)); + el._inputNode.dispatchEvent(new CustomEvent('change', { bubbles: true })); + expect(el.checked).to.be.false; + }); + + it('synchronizes modelValue to checked state and vice versa', async () => { + const el = /** @type {ChoiceInput} */ (await fixture( + html`<${tag} .choiceValue=${'foo'}>`, + )); + expect(el.checked).to.be.false; + expect(el.modelValue).to.deep.equal({ + checked: false, + value: 'foo', + }); + el.checked = true; + expect(el.checked).to.be.true; + expect(el.modelValue).to.deep.equal({ + checked: true, + value: 'foo', + }); + }); + + it('ensures optimal synchronize performance by preventing redundant computation steps', async () => { + /* we are checking private apis here to make sure we do not have cyclical updates + which can be quite common for these type of connected data */ + const el = /** @type {ChoiceInput} */ (await fixture( + html`<${tag} .choiceValue=${'foo'}>`, + )); + expect(el.checked).to.be.false; + + const spyModelCheckedToChecked = sinon.spy(el, '__syncModelCheckedToChecked'); + const spyCheckedToModel = sinon.spy(el, '__syncCheckedToModel'); + el.checked = true; + expect(el.modelValue.checked).to.be.true; + expect(spyModelCheckedToChecked.callCount).to.equal(0); + expect(spyCheckedToModel.callCount).to.equal(1); + + el.modelValue = { value: 'foo', checked: false }; + expect(el.checked).to.be.false; + expect(spyModelCheckedToChecked.callCount).to.equal(1); + expect(spyCheckedToModel.callCount).to.equal(1); + + // not changing values should not trigger any updates + el.checked = false; + el.modelValue = { value: 'foo', checked: false }; + expect(spyModelCheckedToChecked.callCount).to.equal(1); + expect(spyCheckedToModel.callCount).to.equal(1); + }); + + it('synchronizes checked state to [checked] attribute for styling purposes', async () => { + /** @param {ChoiceInput} el */ + const hasAttr = el => el.hasAttribute('checked'); + const el = /** @type {ChoiceInput} */ (await fixture(html`<${tag}>`)); + const elChecked = /** @type {ChoiceInput} */ (await fixture(html` + <${tag} .checked=${true}> + + + `)); + + // Initial values + expect(hasAttr(el)).to.equal(false, 'initial unchecked element'); + expect(hasAttr(elChecked)).to.equal(true, 'initial checked element'); + + // Via user interaction + el._inputNode.click(); + elChecked._inputNode.click(); + await el.updateComplete; + expect(el.checked).to.be.true; + expect(hasAttr(el)).to.equal(true, 'user click checked'); + if (el.type === 'checkbox') { + expect(hasAttr(elChecked)).to.equal(false, 'user click unchecked'); + } + + // reset + el.checked = false; + elChecked.checked = true; + + // Programmatically via checked + el.checked = true; + elChecked.checked = false; + + await el.updateComplete; + expect(hasAttr(el)).to.equal(true, 'programmatically checked'); + expect(hasAttr(elChecked)).to.equal(false, 'programmatically unchecked'); + + // reset + el.checked = false; + elChecked.checked = true; + + // Programmatically via modelValue + el.modelValue = { value: '', checked: true }; + elChecked.modelValue = { value: '', checked: false }; + await el.updateComplete; + expect(hasAttr(el)).to.equal(true, 'modelValue checked'); + expect(hasAttr(elChecked)).to.equal(false, 'modelValue unchecked'); + }); + }); + + describe('Format/parse/serialize loop', () => { + it('creates a modelValue object like { checked: true, value: foo } on init', async () => { + const el = /** @type {ChoiceInput} */ (await fixture( + html`<${tag} .choiceValue=${'foo'}>`, + )); + expect(el.modelValue).deep.equal({ value: 'foo', checked: false }); + + const elChecked = /** @type {ChoiceInput} */ (await fixture(html` + <${tag} .choiceValue=${'foo'} .checked=${true}> + `)); + expect(elChecked.modelValue).deep.equal({ value: 'foo', checked: true }); + }); + + it('creates a formattedValue based on modelValue.value', async () => { + const el = /** @type {ChoiceInput} */ (await fixture(html`<${tag}>`)); + expect(el.formattedValue).to.equal(''); + + const elementWithValue = /** @type {ChoiceInput} */ (await fixture(html` + <${tag} .choiceValue=${'foo'}> + `)); + expect(elementWithValue.formattedValue).to.equal('foo'); + }); + + it('can clear the checked state', async () => { + const el = /** @type {ChoiceInput} */ (await fixture(html`<${tag}>`)); + el.modelValue = { value: 'foo', checked: true }; + el.clear(); + expect(el.modelValue).deep.equal({ value: 'foo', checked: false }); + }); + }); + + describe('Interaction states', () => { + it('is considered prefilled when checked and not considered prefilled when unchecked', async () => { + const el = /** @type {ChoiceInput} */ (await fixture( + html`<${tag} .checked=${true}>`, + )); + expect(el.prefilled).equal(true, 'checked element not considered prefilled'); + + const elUnchecked = /** @type {ChoiceInput} */ (await fixture(html`<${tag}>`)); + expect(elUnchecked.prefilled).equal(false, 'unchecked element not considered prefilled'); + }); + }); + }); +} diff --git a/packages/form-core/test/choice-group/ChoiceInputMixin.test.js b/packages/form-core/test/choice-group/ChoiceInputMixin.test.js index dd43987c7..ceb8a6e7e 100644 --- a/packages/form-core/test/choice-group/ChoiceInputMixin.test.js +++ b/packages/form-core/test/choice-group/ChoiceInputMixin.test.js @@ -1,276 +1,3 @@ -import { html } from '@lion/core'; -import { Required } from '@lion/form-core'; -import { LionInput } from '@lion/input'; -import { expect, fixture } from '@open-wc/testing'; -import sinon from 'sinon'; -import { ChoiceInputMixin } from '../../src/choice-group/ChoiceInputMixin.js'; +import { runChoiceInputMixinSuite } from '../../test-suites/choice-group/ChoiceInputMixin.suite.js'; -describe('ChoiceInputMixin', () => { - class ChoiceClass extends ChoiceInputMixin(LionInput) { - constructor() { - super(); - this.type = 'checkbox'; // could also be 'radio', should be tested in integration test - } - } - customElements.define('choice-input', ChoiceClass); - - it('is hidden when attribute hidden is true', async () => { - const el = /** @type {ChoiceClass} */ (await fixture( - html``, - )); - expect(el).not.to.be.displayed; - }); - - it('has choiceValue', async () => { - const el = /** @type {ChoiceClass} */ (await fixture( - html``, - )); - - expect(el.choiceValue).to.equal('foo'); - expect(el.modelValue).to.deep.equal({ - value: 'foo', - checked: false, - }); - }); - - it('can handle complex data via choiceValue', async () => { - const date = new Date(2018, 11, 24, 10, 33, 30, 0); - - const el = /** @type {ChoiceClass} */ (await fixture( - html``, - )); - - expect(el.choiceValue).to.equal(date); - expect(el.modelValue.value).to.equal(date); - }); - - it('fires one "model-value-changed" event if choiceValue or checked state or modelValue changed', async () => { - let counter = 0; - const el = /** @type {ChoiceClass} */ (await fixture(html` - { - counter += 1; - }} - .choiceValue=${'foo'} - > - `)); - expect(counter).to.equal(1); // undefined to set value - - el.checked = true; - expect(counter).to.equal(2); - - // no change means no event - el.checked = true; - el.choiceValue = 'foo'; - el.modelValue = { value: 'foo', checked: true }; - expect(counter).to.equal(2); - - el.modelValue = { value: 'foo', checked: false }; - expect(counter).to.equal(3); - }); - - it('fires one "user-input-changed" event after user interaction', async () => { - let counter = 0; - const el = /** @type {ChoiceClass} */ (await fixture(html` - - - - `)); - expect(counter).to.equal(0); - // Here we try to mimic user interaction by firing browser events - const nativeInput = el._inputNode; - nativeInput.dispatchEvent(new CustomEvent('input', { bubbles: true })); // fired by (at least) Chrome - expect(counter).to.equal(0); - nativeInput.dispatchEvent(new CustomEvent('change', { bubbles: true })); - expect(counter).to.equal(1); - }); - - it('can be required', async () => { - const el = /** @type {ChoiceClass} */ (await fixture(html` - - `)); - expect(el.hasFeedbackFor).to.include('error'); - expect(el.validationStates.error).to.exist; - expect(el.validationStates.error.Required).to.exist; - el.checked = true; - expect(el.hasFeedbackFor).not.to.include('error'); - expect(el.validationStates.error).to.exist; - expect(el.validationStates.error.Required).not.to.exist; - }); - - describe('Checked state synchronization', () => { - it('synchronizes checked state initially (via attribute or property)', async () => { - const el = /** @type {ChoiceClass} */ (await fixture(``)); - expect(el.checked).to.equal(false, 'initially unchecked'); - - const precheckedElementAttr = /** @type {ChoiceClass} */ (await fixture(html` - - `)); - expect(precheckedElementAttr.checked).to.equal(true, 'initially checked via attribute'); - }); - - it('can be checked and unchecked programmatically', async () => { - const el = /** @type {ChoiceClass} */ (await fixture(``)); - expect(el.checked).to.be.false; - el.checked = true; - expect(el.checked).to.be.true; - - await el.updateComplete; - expect(el._inputNode.checked).to.be.true; - }); - - it('can be checked and unchecked via user interaction', async () => { - const el = /** @type {ChoiceClass} */ (await fixture(``)); - el._inputNode.click(); - expect(el.checked).to.be.true; - el._inputNode.click(); - expect(el.checked).to.be.false; - }); - - it('can not toggle the checked state when disabled via user interaction', async () => { - const el = /** @type {ChoiceClass} */ (await fixture( - html``, - )); - el._inputNode.dispatchEvent(new CustomEvent('change', { bubbles: true })); - expect(el.checked).to.be.false; - }); - - it('synchronizes modelValue to checked state and vice versa', async () => { - const el = /** @type {ChoiceClass} */ (await fixture( - html``, - )); - expect(el.checked).to.be.false; - expect(el.modelValue).to.deep.equal({ - checked: false, - value: 'foo', - }); - el.checked = true; - expect(el.checked).to.be.true; - expect(el.modelValue).to.deep.equal({ - checked: true, - value: 'foo', - }); - }); - - it('ensures optimal synchronize performance by preventing redundant computation steps', async () => { - /* we are checking private apis here to make sure we do not have cyclical updates - which can be quite common for these type of connected data */ - const el = /** @type {ChoiceClass} */ (await fixture( - html``, - )); - expect(el.checked).to.be.false; - - const spyModelCheckedToChecked = sinon.spy(el, '__syncModelCheckedToChecked'); - const spyCheckedToModel = sinon.spy(el, '__syncCheckedToModel'); - el.checked = true; - expect(el.modelValue.checked).to.be.true; - expect(spyModelCheckedToChecked.callCount).to.equal(0); - expect(spyCheckedToModel.callCount).to.equal(1); - - el.modelValue = { value: 'foo', checked: false }; - expect(el.checked).to.be.false; - expect(spyModelCheckedToChecked.callCount).to.equal(1); - expect(spyCheckedToModel.callCount).to.equal(1); - - // not changing values should not trigger any updates - el.checked = false; - el.modelValue = { value: 'foo', checked: false }; - expect(spyModelCheckedToChecked.callCount).to.equal(1); - expect(spyCheckedToModel.callCount).to.equal(1); - }); - - it('synchronizes checked state to [checked] attribute for styling purposes', async () => { - /** @param {ChoiceClass} el */ - const hasAttr = el => el.hasAttribute('checked'); - const el = /** @type {ChoiceClass} */ (await fixture(``)); - const elChecked = /** @type {ChoiceClass} */ (await fixture(html` - - - - `)); - - // Initial values - expect(hasAttr(el)).to.equal(false, 'initial unchecked element'); - expect(hasAttr(elChecked)).to.equal(true, 'initial checked element'); - - // Programmatically via checked - el.checked = true; - elChecked.checked = false; - - await el.updateComplete; - expect(hasAttr(el)).to.equal(true, 'programmatically checked'); - expect(hasAttr(elChecked)).to.equal(false, 'programmatically unchecked'); - - // reset - el.checked = false; - elChecked.checked = true; - - // Via user interaction - el._inputNode.click(); - elChecked._inputNode.click(); - await el.updateComplete; - expect(hasAttr(el)).to.equal(true, 'user click checked'); - expect(hasAttr(elChecked)).to.equal(false, 'user click unchecked'); - - // reset - el.checked = false; - elChecked.checked = true; - - // Programmatically via modelValue - el.modelValue = { value: '', checked: true }; - elChecked.modelValue = { value: '', checked: false }; - await el.updateComplete; - expect(hasAttr(el)).to.equal(true, 'modelValue checked'); - expect(hasAttr(elChecked)).to.equal(false, 'modelValue unchecked'); - }); - }); - - describe('Format/parse/serialize loop', () => { - it('creates a modelValue object like { checked: true, value: foo } on init', async () => { - const el = /** @type {ChoiceClass} */ (await fixture( - html``, - )); - expect(el.modelValue).deep.equal({ value: 'foo', checked: false }); - - const elChecked = /** @type {ChoiceClass} */ (await fixture(html` - - `)); - expect(elChecked.modelValue).deep.equal({ value: 'foo', checked: true }); - }); - - it('creates a formattedValue based on modelValue.value', async () => { - const el = /** @type {ChoiceClass} */ (await fixture(``)); - expect(el.formattedValue).to.equal(''); - - const elementWithValue = /** @type {ChoiceClass} */ (await fixture(html` - - `)); - expect(elementWithValue.formattedValue).to.equal('foo'); - }); - - it('can clear the checked state', async () => { - const el = /** @type {ChoiceClass} */ (await fixture(``)); - el.modelValue = { value: 'foo', checked: true }; - el.clear(); - expect(el.modelValue).deep.equal({ value: 'foo', checked: false }); - }); - }); - - describe('Interaction states', () => { - it('is considered prefilled when checked and not considered prefilled when unchecked', async () => { - const el = /** @type {ChoiceClass} */ (await fixture( - html``, - )); - expect(el.prefilled).equal(true, 'checked element not considered prefilled'); - - const elUnchecked = /** @type {ChoiceClass} */ (await fixture( - ``, - )); - expect(elUnchecked.prefilled).equal(false, 'unchecked element not considered prefilled'); - }); - }); -}); +runChoiceInputMixinSuite(); diff --git a/packages/radio-group/test/lion-radio-group-integrations.test.js b/packages/radio-group/test/lion-radio-group-integrations.test.js new file mode 100644 index 000000000..ad3462b69 --- /dev/null +++ b/packages/radio-group/test/lion-radio-group-integrations.test.js @@ -0,0 +1,9 @@ +import { runChoiceGroupMixinSuite } from '@lion/form-core/test-suites/choice-group/ChoiceGroupMixin.suite.js'; +import '../lion-radio-group.js'; +import '../lion-radio.js'; + +runChoiceGroupMixinSuite({ + parentTagString: 'lion-radio-group', + childTagString: 'lion-radio', + choiceType: 'single', +}); diff --git a/packages/radio-group/test/lion-radio-group.test.js b/packages/radio-group/test/lion-radio-group.test.js index fc76526a8..084b46943 100644 --- a/packages/radio-group/test/lion-radio-group.test.js +++ b/packages/radio-group/test/lion-radio-group.test.js @@ -11,6 +11,46 @@ import '../lion-radio.js'; const fixture = /** @type {(arg: TemplateResult) => Promise} */ (_fixture); describe('', () => { + describe('resetGroup', () => { + // TODO move to FormGroupMixin test suite and let CheckboxGroup make use of them + it('restores to empty modelValue if changes were made', async () => { + const el = await fixture(html` + + + + + `); + el.formElements[0].checked = true; + expect(el.modelValue).to.deep.equal('male'); + + el.resetGroup(); + expect(el.modelValue).to.deep.equal(''); + }); + + it('restores default values if changes were made', async () => { + const el = await fixture(html` + + + + + + `); + el.formElements[0].checked = true; + expect(el.modelValue).to.deep.equal('male'); + + el.resetGroup(); + expect(el.modelValue).to.deep.equal('female'); + + el.formElements[2].checked = true; + expect(el.modelValue).to.deep.equal('other'); + + el.resetGroup(); + await el.formElements[1].updateComplete; + await el.updateComplete; + expect(el.modelValue).to.deep.equal('female'); + }); + }); + it('should have role = radiogroup', async () => { const el = await fixture(html` diff --git a/packages/radio-group/test/lion-radio-integrations.test.js b/packages/radio-group/test/lion-radio-integrations.test.js new file mode 100644 index 000000000..675f7e723 --- /dev/null +++ b/packages/radio-group/test/lion-radio-integrations.test.js @@ -0,0 +1,4 @@ +import '../lion-radio.js'; +import { runChoiceInputMixinSuite } from '@lion/form-core/test-suites/choice-group/ChoiceInputMixin.suite.js'; + +runChoiceInputMixinSuite({ tagString: 'lion-radio' }); diff --git a/packages/radio-group/test/lion-radio.test.js b/packages/radio-group/test/lion-radio.test.js index 4896cc090..0b8927084 100644 --- a/packages/radio-group/test/lion-radio.test.js +++ b/packages/radio-group/test/lion-radio.test.js @@ -1,11 +1,39 @@ -import { expect, fixture } from '@open-wc/testing'; +import { expect, fixture, html } from '@open-wc/testing'; import '../lion-radio.js'; +/** + * @typedef {import('../src/LionRadio').LionRadio} LionRadio + */ + describe('', () => { it('should have type = radio', async () => { - const el = await fixture(` - + const el = await fixture(html` + `); expect(el.getAttribute('type')).to.equal('radio'); }); + + it('can be reset when unchecked by default', async () => { + const el = /** @type {LionRadio} */ (await fixture(html` + + `)); + expect(el._initialModelValue).to.deep.equal({ value: 'male', checked: false }); + el.checked = true; + expect(el.modelValue).to.deep.equal({ value: 'male', checked: true }); + + el.reset(); + expect(el.modelValue).to.deep.equal({ value: 'male', checked: false }); + }); + + it('can be reset when checked by default', async () => { + const el = /** @type {LionRadio} */ (await fixture(html` + + `)); + expect(el._initialModelValue).to.deep.equal({ value: 'male', checked: true }); + el.checked = false; + expect(el.modelValue).to.deep.equal({ value: 'male', checked: false }); + + el.reset(); + expect(el.modelValue).to.deep.equal({ value: 'male', checked: true }); + }); });