fix(form-core): add clear() to choice-group; test integratoons

This commit is contained in:
Thijs Louisse 2021-04-08 09:57:15 +02:00
parent 0fa222c21b
commit 6ae7a5e352
11 changed files with 316 additions and 242 deletions

View file

@ -0,0 +1,6 @@
---
'@lion/form-core': patch
'@lion/form-integrations': patch
---
Add `clear()` interface to choiceGroups

View file

@ -187,6 +187,14 @@ const ChoiceGroupMixinImplementation = superclass =>
super.addFormElement(child, indexToInsertAt);
}
clear() {
if (this.multipleChoice) {
this.modelValue = [];
} else {
this.modelValue = '';
}
}
/**
* @override from FormControlMixin
* @protected

View file

@ -507,6 +507,23 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
}
});
it('can be cleared', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html`
<${parentTag} name="gender[]">
<${childTag} .choiceValue=${'male'}></${childTag}>
<${childTag} .choiceValue=${'female'}></${childTag}>
</${parentTag}>
`));
el.formElements[0].checked = true;
el.clear();
if (cfg.choiceType === 'single') {
expect(el.serializedValue).to.deep.equal('');
} else {
expect(el.serializedValue).to.deep.equal([]);
}
});
describe('multipleChoice', () => {
it('has a single modelValue representing all currently checked values', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html`

View file

@ -28,6 +28,8 @@ export declare class ChoiceGroupHost {
addFormElement(child: FormControlHost, indexToInsertAt: number): void;
clear(): void;
protected _triggerInitialModelValueChangedEvent(): void;
_getFromAllFormElements(property: string, filterCondition: Function): void;

View file

@ -1,4 +1,5 @@
import { expect, fixture, html } from '@open-wc/testing';
import { getAllTagNames } from './helpers/helpers.js';
import './helpers/umbrella-form.js';
import '@lion/dialog/define';
@ -10,7 +11,7 @@ import '@lion/dialog/define';
// Test umbrella form inside dialog
describe('Form inside dialog Integrations', () => {
it('"Successfully registers all form components inside a dialog', async () => {
it('Successfully registers all form components inside a dialog', async () => {
const el = /** @type {LionDialog} */ await fixture(html` <lion-dialog>
<button slot="invoker">Open Dialog</button>
<umbrella-form slot="content"></umbrella-form>
@ -18,68 +19,54 @@ describe('Form inside dialog Integrations', () => {
// @ts-ignore
const formEl = /** @type {LionForm} */ (el._overlayCtrl.contentNode._lionFormNode);
const getTagNames = (/** @type {HTMLElement} */ elm) => [
elm.tagName.toLowerCase(),
// @ts-ignore
...(elm.formElements ? elm.formElements.map(getTagNames).flat() : []),
];
await formEl.registrationComplete;
const registeredEls = getAllTagNames(formEl);
// @ts-ignore
const registeredEls = formEl.formElements.map(getTagNames).flat();
// TODO: fix registration of lion-switch and lion-input-stepper
expect(registeredEls).to.eql([
// [1] In a dialog, these are registered before the rest (which is in chronological dom order)
// Ideally, this should be the same. It would be once the platform allows to create dialogs
// that don't need to move content to the body
'lion-checkbox-group',
' lion-checkbox',
'lion-switch',
// [2] 'the rest' (chronologically ordered registrations)
'lion-fieldset',
'lion-input',
'lion-input',
' lion-input',
' lion-input',
'lion-input-date',
'lion-input-datepicker',
'lion-textarea',
'lion-input-amount',
'lion-input-iban',
'lion-input-email',
'lion-checkbox-group',
'lion-checkbox',
'lion-checkbox',
'lion-checkbox',
' lion-checkbox',
' lion-checkbox',
' lion-checkbox',
'lion-radio-group',
'lion-radio',
'lion-radio',
'lion-radio',
' lion-radio',
' lion-radio',
' lion-radio',
'lion-listbox',
'lion-option',
'lion-option',
'lion-option',
' lion-option',
' lion-option',
' lion-option',
'lion-combobox',
'lion-option',
'lion-option',
'lion-option',
'lion-option',
'lion-option',
'lion-option',
' lion-option',
' lion-option',
' lion-option',
' lion-option',
' lion-option',
' lion-option',
'lion-select-rich',
'lion-option',
'lion-option',
'lion-option',
' lion-option',
' lion-option',
' lion-option',
'lion-select',
'lion-input-range',
'lion-checkbox-group',
'lion-checkbox',
// 'lion-switch',
// 'lion-input-stepper',
// [3] this is where [1] should have been inserted
// [4] more of 'the rest' (chronologically ordered registrations)
'lion-input-stepper',
'lion-textarea',
]);
});

View file

@ -0,0 +1,161 @@
import { elementUpdated, expect, fixture, html } from '@open-wc/testing';
import './helpers/umbrella-form.js';
import { getAllFieldsAndFormGroups } from './helpers/helpers.js';
/**
* @typedef {import('@lion/form-core').LionField} LionField
* @typedef {import('@lion/button').LionButton} LionButton
* @typedef {import('./helpers/umbrella-form.js').UmbrellaForm} UmbrellaForm
*/
const fullyPrefilledSerializedValue = {
full_name: { first_name: 'Lorem', last_name: 'Ipsum' },
date: '2000-12-12',
datepicker: '2020-12-12',
bio: 'Lorem',
money: '12313.12',
iban: '123456',
email: 'a@b.com',
checkers: ['foo', 'bar'],
dinosaurs: 'brontosaurus',
favoriteFruit: 'Banana',
favoriteMovie: 'Rocky',
favoriteColor: 'hotpink',
lyrics: '1',
notifications: {
checked: true,
value: 'Lorem',
},
range: 2.3,
rsvp: 'Lorem',
terms: ['agreed'],
comments: 'Lorem',
};
const fullyChangedSerializedValue = {
full_name: { first_name: 'LoremChanged', last_name: 'IpsumChanged' },
date: '1999-12-12',
datepicker: '1986-12-12',
bio: 'LoremChanged',
money: '9912313.12',
iban: '99123456',
email: 'aChanged@b.com',
checkers: ['foo'],
dinosaurs: '',
favoriteFruit: '',
favoriteMovie: '',
favoriteColor: '',
lyrics: '2',
notifications: {
checked: false,
value: 'Lorem',
},
range: 3.3,
rsvp: 'LoremChanged',
terms: [],
comments: 'LoremChanged',
};
describe(`Submitting/Resetting/Clearing Form`, async () => {
it('pressing submit button of a form should make submitted true for all fields', async () => {
const el = /** @type {UmbrellaForm} */ (await fixture(html`<umbrella-form></umbrella-form>`));
await el.updateComplete;
const formEl = el._lionFormNode;
const allElements = getAllFieldsAndFormGroups(formEl);
allElements.forEach((/** @type {LionField} */ field) => {
if (field.tagName === 'LION-SWITCH') {
// TODO: remove this when this is fixed: https://github.com/ing-bank/lion/issues/1204
return;
}
// TODO: prefer submitted 'false' over 'undefined'
expect(Boolean(field.submitted)).to.be.false;
});
/** @type {LionButton} */ (formEl.querySelector('#submit_button')).click();
await elementUpdated(formEl);
await el.updateComplete;
allElements.forEach((/** @type {LionField} */ field) => {
expect(field.submitted).to.be.true;
});
});
it('calling resetGroup() should reset all metadata (interaction states and initial values)', async () => {
const el = /** @type {UmbrellaForm} */ (await fixture(
html`<umbrella-form .serializedValue="${fullyPrefilledSerializedValue}"></umbrella-form>`,
));
await el.updateComplete;
const formEl = el._lionFormNode;
/** @type {LionButton} */ (formEl.querySelector('#submit_button')).click();
await elementUpdated(formEl);
await formEl.updateComplete;
const allElements = getAllFieldsAndFormGroups(formEl);
allElements.forEach((/** @type {LionField} */ field) => {
// eslint-disable-next-line no-param-reassign
field.touched = true;
// eslint-disable-next-line no-param-reassign
field.dirty = true;
});
formEl.serializedValue = fullyChangedSerializedValue;
allElements.forEach((/** @type {LionField} */ field) => {
expect(field.submitted).to.be.true;
expect(field.touched).to.be.true;
expect(field.dirty).to.be.true;
});
/** @type {LionButton} */ (formEl.querySelector('#reset_button')).click();
await elementUpdated(formEl);
await formEl.updateComplete;
expect(formEl.submitted).to.be.false;
allElements.forEach((/** @type {LionField} */ field) => {
expect(field.submitted).to.be.false;
expect(field.touched).to.be.false;
expect(field.dirty).to.be.false;
});
// TODO: investigate why this doesn't work
// expect(formEl.serializedValue).to.eql(fullyPrefilledSerializedValue);
});
// Wait till ListboxMixin properly clears
it('calling clearGroup() should clear all fields', async () => {
const el = /** @type {UmbrellaForm} */ (await fixture(
html`<umbrella-form .serializedValue="${fullyPrefilledSerializedValue}"></umbrella-form>`,
));
await el.updateComplete;
const formEl = el._lionFormNode;
formEl.clearGroup();
await elementUpdated(formEl);
await formEl.updateComplete;
expect(formEl.serializedValue).to.eql({
full_name: { first_name: '', last_name: '' },
date: '',
datepicker: '',
bio: '',
money: '',
iban: '',
email: '',
checkers: [],
dinosaurs: '',
favoriteFruit: '',
favoriteMovie: '',
favoriteColor: '',
lyrics: '',
notifications: {
checked: false,
value: 'Lorem',
},
range: '',
rsvp: '',
terms: [],
comments: '',
});
});
});

View file

@ -1,4 +1,5 @@
import { expect, fixture, html } from '@open-wc/testing';
import { getAllTagNames } from './helpers/helpers.js';
import './helpers/umbrella-form.js';
/**
@ -28,6 +29,8 @@ describe('Form Integrations', () => {
lyrics: '1',
range: 2.3,
terms: [],
notifications: { value: '', checked: false },
rsvp: '',
comments: '',
});
});
@ -36,6 +39,7 @@ describe('Form Integrations', () => {
const el = /** @type {UmbrellaForm} */ (await fixture(html`<umbrella-form></umbrella-form>`));
await el.updateComplete;
const formEl = el._lionFormNode;
expect(formEl.formattedValue).to.eql({
full_name: { first_name: '', last_name: '' },
date: '12/12/2000',
@ -52,6 +56,8 @@ describe('Form Integrations', () => {
lyrics: '1',
range: 2.3,
terms: [],
notifications: '',
rsvp: '',
comments: '',
});
});
@ -86,73 +92,52 @@ describe('Form Integrations', () => {
});
});
it('"Successfully registers all form components', async () => {
it('Successfully registers all form components', async () => {
const el = /** @type {UmbrellaForm} */ await fixture(html`<umbrella-form></umbrella-form>`);
// @ts-ignore
const formEl = /** @type {LionForm} */ (el._lionFormNode);
const getTagNames = (/** @type {HTMLElement} */ elm) => [
elm.tagName.toLowerCase(),
// @ts-ignore
...(elm.formElements ? elm.formElements.map(getTagNames).flat() : []),
];
await formEl.registrationComplete;
const registeredEls = getAllTagNames(formEl);
// @ts-ignore
const registeredEls = formEl.formElements.map(getTagNames).flat();
// TODO: fix registration of lion-switch and lion-input-stepper
expect(registeredEls).to.eql([
'lion-fieldset',
'lion-input',
'lion-input',
' lion-input',
' lion-input',
'lion-input-date',
'lion-input-datepicker',
'lion-textarea',
'lion-input-amount',
'lion-input-iban',
'lion-input-email',
'lion-checkbox-group',
'lion-checkbox',
'lion-checkbox',
'lion-checkbox',
' lion-checkbox',
' lion-checkbox',
' lion-checkbox',
'lion-radio-group',
'lion-radio',
'lion-radio',
'lion-radio',
' lion-radio',
' lion-radio',
' lion-radio',
'lion-listbox',
'lion-option',
'lion-option',
'lion-option',
' lion-option',
' lion-option',
' lion-option',
'lion-combobox',
'lion-option',
'lion-option',
'lion-option',
'lion-option',
'lion-option',
'lion-option',
' lion-option',
' lion-option',
' lion-option',
' lion-option',
' lion-option',
' lion-option',
'lion-select-rich',
'lion-option',
'lion-option',
'lion-option',
' lion-option',
' lion-option',
' lion-option',
'lion-select',
'lion-input-range',
'lion-checkbox-group',
'lion-checkbox',
// 'lion-switch',
// 'lion-input-stepper',
' lion-checkbox',
'lion-switch',
'lion-input-stepper',
'lion-textarea',
]);
});

View file

@ -1,143 +0,0 @@
import '@lion/button/define';
import '@lion/checkbox-group/define';
import { MinLength, Required } from '@lion/form-core';
import '@lion/form/define';
import '@lion/input-amount/define';
import '@lion/input-date/define';
import '@lion/input-datepicker/define';
import '@lion/input-email/define';
import '@lion/input-iban/define';
import '@lion/input-range/define';
import '@lion/input/define';
import '@lion/radio-group/define';
import '@lion/select/define';
import '@lion/switch/define';
import '@lion/textarea/define';
import { elementUpdated, expect, fixture, html } from '@open-wc/testing';
describe(`Submitting/Resetting Form`, async () => {
/** @type {import('@lion/form').LionForm} */
let el;
beforeEach(async () => {
el = await fixture(html`
<lion-form id="form_test" responsive>
<form>
<lion-input
name="first_name"
label="First Name"
.validators="${[new Required()]}"
></lion-input>
<lion-input
name="last_name"
label="Last Name"
.validators="${[new Required()]}"
></lion-input>
<lion-input-date
name="start-date"
label="Start date"
.validators="${[new Required()]}"
></lion-input-date>
<lion-input-datepicker
name="end-date"
label="End date"
.validators="${[new Required()]}"
></lion-input-datepicker>
<lion-textarea
name="bio"
label="Biography"
.validators="${[new Required(), new MinLength(10)]}"
help-text="Please enter at least 10 characters"
></lion-textarea>
<lion-input-amount
.validators="${[new Required()]}"
name="money"
label="Money"
></lion-input-amount>
<lion-input-iban
.validators="${[new Required()]}"
name="iban"
label="Iban"
></lion-input-iban>
<lion-input-email
.validators="${[new Required()]}"
name="email"
label="Email"
></lion-input-email>
<lion-checkbox-group
label="What do you like?"
name="checkers[]"
.validators="${[new Required()]}"
>
<lion-checkbox .choiceValue=${'foo'} label="I like foo"></lion-checkbox>
<lion-checkbox .choiceValue=${'bar'} label="I like bar"></lion-checkbox>
<lion-checkbox .choiceValue=${'baz'} label="I like baz"></lion-checkbox>
</lion-checkbox-group>
<lion-radio-group
name="dinosaurs"
label="Favorite dinosaur"
.validators="${[new Required()]}"
>
<lion-radio .choiceValue=${'allosaurus'} label="allosaurus"></lion-radio>
<lion-radio .choiceValue=${'brontosaurus'} label="brontosaurus"></lion-radio>
<lion-radio .choiceValue=${'diplodocus'} label="diplodocus"></lion-radio>
</lion-radio-group>
<lion-select label="Lyrics" name="lyrics" .validators="${[new Required()]}">
<select slot="input">
<option value="1">Fire up that loud</option>
<option value="2">Another round of shots...</option>
<option value="3">Drop down for what?</option>
</select>
</lion-select>
<lion-input-range
.validators="${[new Required()]}"
name="range"
min="1"
max="5"
unit="%"
step="0.1"
label="Input range"
></lion-input-range>
<lion-checkbox-group
name="terms[]"
.validators="${[
new Required(null, { getMessage: () => 'You are not allowed to read them' }),
]}"
>
<lion-checkbox label="I blindly accept all terms and conditions"></lion-checkbox>
</lion-checkbox-group>
<lion-textarea
.validators="${[new Required()]}"
name="comments"
label="Comments"
></lion-textarea>
<div class="buttons">
<lion-button id="submit_button" type="submit" raised>Submit</lion-button>
<lion-button id="reset_button" type="reset" raised> Reset </lion-button>
</div>
</form>
</lion-form>
`);
});
it('Submitting a form should make submitted true for all fields', async () => {
/** @type {import('@lion/button').LionButton} */ (el.querySelector('#submit_button')).click();
await elementUpdated(el);
await el.updateComplete;
el.formElements.forEach(field => {
expect(field.submitted).to.be.true;
});
});
it('Resetting a form should reset metadata of all fields', async () => {
/** @type {import('@lion/button').LionButton} */ (el.querySelector('#submit_button')).click();
/** @type {import('@lion/button').LionButton} */ (el.querySelector('#reset_button')).click();
await elementUpdated(el);
await el.updateComplete;
expect(el.submitted).to.be.false;
el.formElements.forEach(field => {
expect(field.submitted).to.be.false;
expect(field.touched).to.be.false;
expect(field.dirty).to.be.false;
});
});
});

View file

@ -0,0 +1,40 @@
/**
* @typedef {import('@lion/form').LionForm} LionForm
* @typedef {import('@lion/form-core').LionField} LionField
*/
/**
* @param {LionForm} formGroupEl
*/
export function getAllFormElements(formGroupEl) {
const getElms = (/** @type {HTMLElement} */ elm) => [
elm,
// @ts-ignore
...(elm.formElements ? elm.formElements.map(getElms).flat() : []),
];
// @ts-ignore
return formGroupEl.formElements.map(elem => getElms(elem)).flat();
}
/**
* @param {LionForm} formGroupEl
*/
export function getAllTagNames(formGroupEl) {
const getTagNames = (/** @type {HTMLElement} */ elm, lvl = 0) => [
` `.repeat(lvl) + elm.tagName.toLowerCase(),
// @ts-ignore
...(elm.formElements ? elm.formElements.map(elem => getTagNames(elem, lvl + 1)).flat() : []),
];
// @ts-ignore
return formGroupEl.formElements.map(elem => getTagNames(elem)).flat();
}
/**
* @param {LionForm} formGroupEl
*/
export function getAllFieldsAndFormGroups(formGroupEl) {
const allElements = getAllFormElements(formGroupEl);
return allElements.filter((/** @type {LionField} */ elm) => elm.tagName !== 'LION-OPTION');
}

View file

@ -17,6 +17,8 @@ import '@lion/combobox/define';
import '@lion/input-range/define';
import '@lion/textarea/define';
import '@lion/button/define';
import '@lion/switch/define';
import '@lion/input-stepper/define';
export class UmbrellaForm extends LitElement {
get _lionFormNode() {
@ -131,7 +133,10 @@ export class UmbrellaForm extends LitElement {
name="terms"
.validators="${[new Required()]}"
>
<lion-checkbox label="I blindly accept all terms and conditions"></lion-checkbox>
<lion-checkbox
.choiceValue="agreed"
label="I blindly accept all terms and conditions"
></lion-checkbox>
</lion-checkbox-group>
<lion-switch name="notifications" label="Notifications"></lion-switch>
<lion-input-stepper max="5" min="0" name="rsvp">
@ -140,13 +145,14 @@ export class UmbrellaForm extends LitElement {
</lion-input-stepper>
<lion-textarea name="comments" label="Comments"></lion-textarea>
<div class="buttons">
<lion-button raised>Submit</lion-button>
<lion-button id="submit_button" raised>Submit</lion-button>
<lion-button
id="reset_button"
type="button"
raised
@click=${(/** @type {Event} */ ev) =>
// @ts-ignore
ev.currentTarget.parentElement.parentElement.parentElement.resetGroup()}
@click="${() => {
this._lionFormNode.resetGroup();
}}"
>Reset</lion-button
>
</div>

View file

@ -10,11 +10,14 @@ import '@lion/input-datepicker/define';
import '@lion/input-email/define';
import '@lion/input-iban/define';
import '@lion/input-range/define';
import '@lion/input-stepper/define';
import '@lion/textarea/define';
import '@lion/checkbox-group/define';
import '@lion/radio-group/define';
import '@lion/switch/define';
import '@lion/select/define';
@ -343,11 +346,13 @@ describe('detail.isTriggeredByUser', () => {
'input-email',
'input-iban',
'input-range',
'input-stepper',
'textarea',
// 1b) Choice Fields
'option',
'checkbox',
'radio',
'switch',
// 1c) Choice Group Fields
'select',
'listbox',