This commit adds the exports entry in each package.json, exposing the public parts of each package and hiding the private ones.
512 lines
17 KiB
JavaScript
512 lines
17 KiB
JavaScript
import { expect, html, unsafeStatic, fixture } from '@open-wc/testing';
|
|
|
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
import sinon from 'sinon';
|
|
|
|
import '@lion/input/lion-input';
|
|
import '@lion/input-amount/lion-input-amount';
|
|
import '@lion/input-date/lion-input-date';
|
|
import '@lion/input-datepicker/lion-input-datepicker';
|
|
import '@lion/input-email/lion-input-email';
|
|
import '@lion/input-iban/lion-input-iban';
|
|
import '@lion/input-range/lion-input-range';
|
|
import '@lion/textarea/lion-textarea';
|
|
|
|
import '@lion/checkbox-group/lion-checkbox-group';
|
|
import '@lion/checkbox-group/lion-checkbox';
|
|
|
|
import '@lion/radio-group/lion-radio-group';
|
|
import '@lion/radio-group/lion-radio';
|
|
|
|
import '@lion/select/lion-select';
|
|
|
|
import '@lion/combobox/lion-combobox';
|
|
import '@lion/listbox/lion-listbox';
|
|
import '@lion/listbox/lion-option';
|
|
import '@lion/select-rich/lion-select-rich';
|
|
|
|
import '@lion/fieldset/lion-fieldset';
|
|
import '@lion/form/lion-form';
|
|
import '@lion/form-core/lion-field';
|
|
|
|
/**
|
|
* @typedef {import('@lion/core').LitElement} LitElement
|
|
* @typedef {import('@lion/form-core').LionField} LionField
|
|
* @typedef {import('@lion/form-core/types/FormControlMixinTypes').FormControlHost & HTMLElement & {__parentFormGroup?: HTMLElement, checked?: boolean, disabled: boolean, hasFeedbackFor: string[], makeRequestToBeDisabled: Function }} FormControl
|
|
* @typedef {import('@lion/input').LionInput} LionInput
|
|
* @typedef {import('@lion/select').LionSelect} LionSelect
|
|
* @typedef {import('@lion/listbox').LionOption} LionOption
|
|
*/
|
|
|
|
const featureName = 'model value';
|
|
|
|
const getFirstPaintTitle = /** @param {number} count */ count =>
|
|
`should dispatch ${count} time(s) on first paint`;
|
|
const getInteractionTitle = /** @param {number} count */ count =>
|
|
`should dispatch ${count} time(s) on interaction`;
|
|
|
|
const firstStampCount = 1;
|
|
const interactionCount = 1;
|
|
|
|
/**
|
|
* @param {string} tagname
|
|
* @param {number} count
|
|
*/
|
|
const fieldDispatchesCountOnFirstPaint = (tagname, count) => {
|
|
const tag = unsafeStatic(tagname);
|
|
const spy = sinon.spy();
|
|
it(getFirstPaintTitle(count), async () => {
|
|
await fixture(html`<${tag} @model-value-changed="${spy}"></${tag}>`);
|
|
expect(spy.callCount).to.equal(count);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param {string} tagname
|
|
* @param {number} count
|
|
*/
|
|
const fieldDispatchesCountOnInteraction = (tagname, count) => {
|
|
const tag = unsafeStatic(tagname);
|
|
const spy = sinon.spy();
|
|
it(getInteractionTitle(count), async () => {
|
|
const el = /** @type {LionField} */ (await fixture(html`<${tag}></${tag}>`));
|
|
el.addEventListener('model-value-changed', spy);
|
|
// TODO: discuss if this is the "correct" way to interact with component
|
|
el.modelValue = 'foo';
|
|
await el.updateComplete;
|
|
expect(spy.callCount).to.equal(count);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param {string} tagname
|
|
* @param {number} count
|
|
*/
|
|
const choiceDispatchesCountOnFirstPaint = (tagname, count) => {
|
|
const tag = unsafeStatic(tagname);
|
|
const spy = sinon.spy();
|
|
it(getFirstPaintTitle(count), async () => {
|
|
await fixture(html`<${tag} @model-value-changed="${spy}" .choiceValue="${'option'}"></${tag}>`);
|
|
expect(spy.callCount).to.equal(count);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param {string} tagname
|
|
* @param {number} count
|
|
*/
|
|
const choiceDispatchesCountOnInteraction = (tagname, count) => {
|
|
const tag = unsafeStatic(tagname);
|
|
const spy = sinon.spy();
|
|
it(getInteractionTitle(count), async () => {
|
|
const el = /** @type {HTMLElement & {checked: boolean}} */ (await fixture(
|
|
html`<${tag} .choiceValue="${'option'}"></${tag}>`,
|
|
));
|
|
el.addEventListener('model-value-changed', spy);
|
|
el.checked = true;
|
|
expect(spy.callCount).to.equal(count);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param {string} groupTagname
|
|
* @param {string} itemTagname
|
|
* @param {number} count
|
|
*/
|
|
const choiceGroupDispatchesCountOnFirstPaint = (groupTagname, itemTagname, count) => {
|
|
const groupTag = unsafeStatic(groupTagname);
|
|
const itemTag = unsafeStatic(itemTagname);
|
|
it(getFirstPaintTitle(count), async () => {
|
|
const spy = sinon.spy();
|
|
await fixture(html`
|
|
<${groupTag} @model-value-changed="${spy}">
|
|
<${itemTag} .choiceValue="${'option1'}"></${itemTag}>
|
|
<${itemTag} .choiceValue="${'option2'}"></${itemTag}>
|
|
<${itemTag} .choiceValue="${'option3'}"></${itemTag}>
|
|
</${groupTag}>
|
|
`);
|
|
|
|
expect(spy.callCount).to.equal(count);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param {string} groupTagname
|
|
* @param {string} itemTagname
|
|
* @param {number} count
|
|
*/
|
|
const choiceGroupDispatchesCountOnInteraction = (groupTagname, itemTagname, count) => {
|
|
const groupTag = unsafeStatic(groupTagname);
|
|
const itemTag = unsafeStatic(itemTagname);
|
|
it(getInteractionTitle(count), async () => {
|
|
const spy = sinon.spy();
|
|
const el = await fixture(html`
|
|
<${groupTag}>
|
|
<${itemTag} .choiceValue="${'option1'}"></${itemTag}>
|
|
<${itemTag} .choiceValue="${'option2'}"></${itemTag}>
|
|
<${itemTag} .choiceValue="${'option3'}"></${itemTag}>
|
|
</${groupTag}>
|
|
`);
|
|
|
|
el.addEventListener('model-value-changed', spy);
|
|
const option2 = /** @type {HTMLElement & {checked: boolean}} */ (el.querySelector(
|
|
`${itemTagname}:nth-child(2)`,
|
|
));
|
|
option2.checked = true;
|
|
expect(spy.callCount).to.equal(count);
|
|
|
|
spy.resetHistory();
|
|
|
|
const option3 = /** @type {HTMLElement & {checked: boolean}} */ (el.querySelector(
|
|
`${itemTagname}:nth-child(3)`,
|
|
));
|
|
option3.checked = true;
|
|
expect(spy.callCount).to.equal(count);
|
|
});
|
|
};
|
|
|
|
[
|
|
'input',
|
|
'input-amount',
|
|
'input-date',
|
|
'input-datepicker',
|
|
'input-email',
|
|
'input-iban',
|
|
'input-range',
|
|
'textarea',
|
|
].forEach(chunk => {
|
|
const tagname = `lion-${chunk}`;
|
|
describe(`${tagname}`, () => {
|
|
describe(featureName, () => {
|
|
fieldDispatchesCountOnFirstPaint(tagname, firstStampCount);
|
|
fieldDispatchesCountOnInteraction(tagname, interactionCount);
|
|
});
|
|
});
|
|
});
|
|
|
|
['checkbox', 'radio'].forEach(chunk => {
|
|
const groupTagname = `lion-${chunk}-group`;
|
|
const itemTagname = `lion-${chunk}`;
|
|
|
|
describe(`${itemTagname}`, () => {
|
|
describe(featureName, () => {
|
|
choiceDispatchesCountOnFirstPaint(itemTagname, firstStampCount);
|
|
choiceDispatchesCountOnInteraction(itemTagname, interactionCount);
|
|
});
|
|
});
|
|
|
|
describe(`${groupTagname}`, () => {
|
|
describe(featureName, () => {
|
|
choiceGroupDispatchesCountOnFirstPaint(groupTagname, itemTagname, firstStampCount);
|
|
choiceGroupDispatchesCountOnInteraction(groupTagname, itemTagname, interactionCount);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('lion-select', () => {
|
|
describe(featureName, () => {
|
|
it(getFirstPaintTitle(firstStampCount), async () => {
|
|
const spy = sinon.spy();
|
|
await fixture(html`
|
|
<lion-select @model-value-changed="${spy}">
|
|
<select slot="input">
|
|
<option value="option1"></option>
|
|
<option value="option2"></option>
|
|
<option value="option3"></option>
|
|
</select>
|
|
</lion-select>
|
|
`);
|
|
expect(spy.callCount).to.equal(firstStampCount);
|
|
});
|
|
|
|
it(getInteractionTitle(interactionCount), async () => {
|
|
const spy = sinon.spy();
|
|
const el = /** @type {LionSelect} */ (await fixture(html`
|
|
<lion-select>
|
|
<select slot="input">
|
|
<option value="option1"></option>
|
|
<option value="option2"></option>
|
|
<option value="option3"></option>
|
|
</select>
|
|
</lion-select>
|
|
`));
|
|
el.addEventListener('model-value-changed', spy);
|
|
const option2 = /** @type {HTMLOptionElement} */ (el.querySelector('option:nth-child(2)'));
|
|
|
|
// mimic user input
|
|
option2.selected = true;
|
|
el._inputNode.dispatchEvent(new CustomEvent('change'));
|
|
|
|
expect(spy.callCount).to.equal(interactionCount);
|
|
|
|
spy.resetHistory();
|
|
|
|
const option3 = /** @type {HTMLOptionElement} */ (el.querySelector('option:nth-child(3)'));
|
|
|
|
// mimic user input
|
|
option3.selected = true;
|
|
el._inputNode.dispatchEvent(new CustomEvent('change'));
|
|
|
|
expect(spy.callCount).to.equal(interactionCount);
|
|
});
|
|
});
|
|
});
|
|
|
|
['combobox', 'listbox', 'select-rich'].forEach(chunk => {
|
|
const tagname = `lion-${chunk}`;
|
|
const tag = unsafeStatic(tagname);
|
|
describe(`${tagname}`, () => {
|
|
describe(featureName, () => {
|
|
it(getFirstPaintTitle(firstStampCount), async () => {
|
|
const spy = sinon.spy();
|
|
await fixture(html`
|
|
<${tag} @model-value-changed="${spy}">
|
|
<lion-option .choiceValue="${'option1'}"></lion-option>
|
|
<lion-option .choiceValue="${'option2'}"></lion-option>
|
|
<lion-option .choiceValue="${'option3'}"></lion-option>
|
|
</${tag}>
|
|
`);
|
|
|
|
expect(spy.callCount).to.equal(firstStampCount);
|
|
});
|
|
|
|
it(getInteractionTitle(interactionCount), async () => {
|
|
const spy = sinon.spy();
|
|
const el = await fixture(html`
|
|
<${tag}>
|
|
<lion-option .choiceValue="${'option1'}">Option 1</lion-option>
|
|
<lion-option .choiceValue="${'option2'}">Option 2</lion-option>
|
|
<lion-option .choiceValue="${'option3'}">Option 3</lion-option>
|
|
</${tag}>
|
|
`);
|
|
|
|
el.addEventListener('model-value-changed', spy);
|
|
const option2 = /** @type {LionOption} */ (el.querySelector('lion-option:nth-child(2)'));
|
|
option2.checked = true;
|
|
expect(spy.callCount).to.equal(interactionCount);
|
|
|
|
spy.resetHistory();
|
|
|
|
const option3 = /** @type {LionOption} */ (el.querySelector('lion-option:nth-child(3)'));
|
|
option3.checked = true;
|
|
expect(spy.callCount).to.equal(interactionCount);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('lion-fieldset', () => {
|
|
describe(featureName, () => {
|
|
it(getFirstPaintTitle(firstStampCount), async () => {
|
|
const spy = sinon.spy();
|
|
await fixture(html`
|
|
<lion-fieldset name="parent" @model-value-changed="${spy}">
|
|
<lion-input name="input"></lion-input>
|
|
</lion-fieldset>
|
|
`);
|
|
|
|
expect(spy.callCount).to.equal(firstStampCount);
|
|
});
|
|
|
|
it(getInteractionTitle(interactionCount), async () => {
|
|
const spy = sinon.spy();
|
|
const el = await fixture(html`
|
|
<lion-fieldset name="parent">
|
|
<lion-input name="input"></lion-input>
|
|
</lion-fieldset>
|
|
`);
|
|
|
|
el.addEventListener('model-value-changed', spy);
|
|
const input = /** @type {LionInput} */ (el.querySelector('lion-input'));
|
|
input.modelValue = 'foo';
|
|
expect(spy.callCount).to.equal(interactionCount);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('detail.isTriggeredByUser', () => {
|
|
const allFormControls = [
|
|
// 1) Fields
|
|
'field',
|
|
// 1a) Input Fields
|
|
'input',
|
|
'input-amount',
|
|
'input-date',
|
|
'input-datepicker',
|
|
'input-email',
|
|
'input-iban',
|
|
'input-range',
|
|
'textarea',
|
|
// 1b) Choice Fields
|
|
'option',
|
|
'checkbox',
|
|
'radio',
|
|
// 1c) Choice Group Fields
|
|
'select',
|
|
'listbox',
|
|
'select-rich',
|
|
'combobox',
|
|
// 2) FormGroups
|
|
// 2a) Choice FormGroups
|
|
'checkbox-group',
|
|
'radio-group',
|
|
// 2v) Fieldset
|
|
'fieldset',
|
|
// 2c) Form
|
|
'form',
|
|
];
|
|
|
|
/**
|
|
* "isTriggeredByUser" for different types of fields:
|
|
*
|
|
* RegularField:
|
|
* - true: when change/input (c.q. user-input-changed) fired
|
|
* - false: when .modelValue set programmatically
|
|
*
|
|
* ChoiceField:
|
|
* - true: when 'change' event fired
|
|
* - false: when .modelValue (or checked) set programmatically
|
|
*
|
|
* OptionChoiceField:
|
|
* - true: when 'click' event fired
|
|
* - false: when .modelValue (or checked) set programmatically
|
|
*
|
|
* ChoiceGroupField (listbox, select-rich, combobox, radio-group, checkbox-group):
|
|
* - true: when child formElement condition for ChoiceField(Option) is met
|
|
* - false: when child formElement condition for ChoiceField(Option) is not met
|
|
*
|
|
* FormOrFieldset (fieldset, form):
|
|
* - true: when child formElement condition for RegularField is met
|
|
* - false: when child formElement condition for RegularField is not met
|
|
*/
|
|
|
|
const featureDetectChoiceField = /** @param {HTMLElement} el */ el =>
|
|
'checked' in el && 'choiceValue' in el;
|
|
const featureDetectOptionChoiceField = /** @param {HTMLElement} el */ el => 'active' in el;
|
|
|
|
/**
|
|
* @param {FormControl} el
|
|
* @returns {'RegularField'|'ChoiceField'|'OptionChoiceField'|'ChoiceGroupField'|'FormOrFieldset'}
|
|
*/
|
|
function detectType(el) {
|
|
if (el._repropagationRole === 'child') {
|
|
if (featureDetectChoiceField(el)) {
|
|
return featureDetectOptionChoiceField(el) ? 'OptionChoiceField' : 'ChoiceField';
|
|
}
|
|
return 'RegularField';
|
|
}
|
|
return el._repropagationRole === 'choice-group' ? 'ChoiceGroupField' : 'FormOrFieldset';
|
|
}
|
|
|
|
/**
|
|
* @param {FormControl & {value: string}} el
|
|
* @param {string} newViewValue
|
|
*/
|
|
function mimicUserInput(el, newViewValue) {
|
|
const type = detectType(el);
|
|
let userInputEv;
|
|
if (type === 'RegularField') {
|
|
userInputEv = el._inputNode.tagName === 'SELECT' ? 'change' : 'input';
|
|
el.value = newViewValue; // eslint-disable-line no-param-reassign
|
|
el._inputNode.dispatchEvent(new Event(userInputEv, { bubbles: true }));
|
|
} else if (type === 'ChoiceField') {
|
|
el._inputNode.dispatchEvent(new Event('change', { bubbles: true }));
|
|
} else if (type === 'OptionChoiceField') {
|
|
el.dispatchEvent(new Event('click', { bubbles: true }));
|
|
}
|
|
}
|
|
|
|
allFormControls.forEach(controlName => {
|
|
it(`lion-${controlName} adds "detail.isTriggeredByUser" to model-value-changed event`, async () => {
|
|
const spy = sinon.spy();
|
|
|
|
const tagname = `lion-${controlName}`;
|
|
const tag = unsafeStatic(tagname);
|
|
let childrenEl;
|
|
if (controlName === 'select') {
|
|
childrenEl = await fixture(
|
|
html`<select slot="input">
|
|
<option value="x"></option>
|
|
</select>`,
|
|
);
|
|
} else if (controlName === 'form') {
|
|
childrenEl = await fixture(html`<form></form>`);
|
|
} else if (controlName === 'field') {
|
|
childrenEl = await fixture(html`<input slot="input" />`);
|
|
}
|
|
|
|
const el = /** @type {LitElement & FormControl & {value: string} & {registrationComplete: Promise<boolean>} & {formElements: Array.<FormControl & {value: string}>}} */ (await fixture(
|
|
html`<${tag}>${childrenEl}</${tag}>`,
|
|
));
|
|
await el.registrationComplete;
|
|
el.addEventListener('model-value-changed', spy);
|
|
|
|
/**
|
|
* @param {FormControl & {value: string}} formControl
|
|
*/
|
|
function expectCorrectEventMetaRegularField(formControl) {
|
|
mimicUserInput(formControl, 'userValue');
|
|
expect(spy.firstCall.args[0].detail.isTriggeredByUser).to.be.true;
|
|
// eslint-disable-next-line no-param-reassign
|
|
formControl.modelValue = 'programmaticValue';
|
|
expect(spy.secondCall.args[0].detail.isTriggeredByUser).to.be.false;
|
|
}
|
|
|
|
/**
|
|
* @param {FormControl & {value: string}} formControl
|
|
*/
|
|
function resetChoiceFieldToForceRepropagation(formControl) {
|
|
// eslint-disable-next-line no-param-reassign
|
|
formControl.checked = false;
|
|
spy.resetHistory();
|
|
}
|
|
|
|
/**
|
|
* @param {FormControl & {value: string}} formControl
|
|
*/
|
|
function expectCorrectEventMetaChoiceField(formControl) {
|
|
resetChoiceFieldToForceRepropagation(formControl);
|
|
mimicUserInput(formControl, 'userValue');
|
|
expect(spy.firstCall.args[0].detail.isTriggeredByUser).to.be.true;
|
|
|
|
resetChoiceFieldToForceRepropagation(formControl);
|
|
// eslint-disable-next-line no-param-reassign
|
|
formControl.checked = true;
|
|
expect(spy.firstCall.args[0].detail.isTriggeredByUser).to.be.false;
|
|
|
|
// eslint-disable-next-line no-param-reassign
|
|
formControl.modelValue = { value: 'programmaticValue', checked: false };
|
|
expect(spy.secondCall.args[0].detail.isTriggeredByUser).to.be.false;
|
|
}
|
|
|
|
// 1. Derive the type of field we're dealing with
|
|
const type = detectType(el);
|
|
if (type === 'RegularField') {
|
|
expectCorrectEventMetaRegularField(el);
|
|
} else if (type === 'ChoiceField' || type === 'OptionChoiceField') {
|
|
expectCorrectEventMetaChoiceField(el);
|
|
} else if (type === 'ChoiceGroupField') {
|
|
let childName = 'option';
|
|
if (controlName.endsWith('-group')) {
|
|
[childName] = controlName.split('-group');
|
|
}
|
|
const childTagName = `lion-${childName}`;
|
|
const childTag = unsafeStatic(childTagName);
|
|
const childrenEls = await fixture(
|
|
html`<div><${childTag}></${childTag}><${childTag}></${childTag}></div>`,
|
|
);
|
|
el.appendChild(childrenEls);
|
|
await el.registrationComplete;
|
|
expectCorrectEventMetaChoiceField(el.formElements[0]);
|
|
} else if (type === 'FormOrFieldset') {
|
|
const childrenEls = await fixture(
|
|
html`<div><lion-input name="one"></lion-input><lion-input name="two"></lion-input></div>`,
|
|
);
|
|
el.appendChild(childrenEls);
|
|
await el.registrationComplete;
|
|
await el.updateComplete;
|
|
expectCorrectEventMetaRegularField(el.formElements[0]);
|
|
}
|
|
});
|
|
});
|
|
});
|