chore(field): introduction test suites (for InteractionStateMixin)
This commit is contained in:
parent
93ce388403
commit
22bb75a868
6 changed files with 260 additions and 190 deletions
23
.eslintrc.js
23
.eslintrc.js
|
|
@ -1,3 +1,26 @@
|
|||
module.exports = {
|
||||
extends: ['@open-wc/eslint-config', 'eslint-config-prettier'].map(require.resolve),
|
||||
rules: {
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
{
|
||||
devDependencies: [
|
||||
'**/test-suites/**/*.js',
|
||||
'**/test/**/*.js',
|
||||
'**/stories/**/*.js',
|
||||
'**/*.config.js',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/test-suites/**/*.js', '**/test/**/*.js', '**/stories/**/*.js', '**/*.config.js'],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
'no-unused-expressions': 'off',
|
||||
'class-methods-use-this': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
"src",
|
||||
"stories",
|
||||
"test",
|
||||
"test-suites",
|
||||
"translations",
|
||||
"*.js"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
|
|||
import { Unparseable } from '@lion/validate';
|
||||
|
||||
/**
|
||||
* `InteractionStateMixin` adds meta information about touched and dirty states, that can
|
||||
* @desc `InteractionStateMixin` adds meta information about touched and dirty states, that can
|
||||
* be read by other form components (ing-uic-input-error for instance, uses the touched state
|
||||
* to determine whether an error message needs to be shown).
|
||||
* Interaction states will be set when a user:
|
||||
* - leaves a form field(blur) -> 'touched' will be set to true. 'prefilled' when a
|
||||
* field is left non-empty
|
||||
* - on keyup (actually, on the model-value-changed event) -> 'dirty' will be set to true
|
||||
* @param {HTMLElement} superclass
|
||||
*/
|
||||
export const InteractionStateMixin = dedupeMixin(
|
||||
superclass =>
|
||||
|
|
@ -58,13 +59,15 @@ export const InteractionStateMixin = dedupeMixin(
|
|||
if (modelValue instanceof Unparseable) {
|
||||
value = modelValue.viewValue;
|
||||
}
|
||||
// Checks for empty objects and arrays
|
||||
// Checks for empty platform types: Objects, Arrays, Dates
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return !!Object.keys(value).length;
|
||||
}
|
||||
// eslint-disable-next-line no-mixed-operators
|
||||
// Checks for empty platform types: Numbers, Booleans
|
||||
const isNumberValue = typeof value === 'number' && (value === 0 || Number.isNaN(value));
|
||||
const isBooleanValue = typeof value === 'boolean' && value === false;
|
||||
|
||||
return !!value || isNumberValue || isBooleanValue;
|
||||
}
|
||||
|
||||
|
|
|
|||
207
packages/field/test-suites/InteractionStateMixin.suite.js
Normal file
207
packages/field/test-suites/InteractionStateMixin.suite.js
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
import {
|
||||
expect,
|
||||
fixture,
|
||||
unsafeStatic,
|
||||
html,
|
||||
defineCE,
|
||||
triggerFocusFor,
|
||||
triggerBlurFor,
|
||||
} from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import { LitElement } from '@lion/core';
|
||||
import { InteractionStateMixin } from '../src/InteractionStateMixin.js';
|
||||
|
||||
export function runInteractionStateMixinSuite(customConfig) {
|
||||
const cfg = {
|
||||
tagString: null,
|
||||
allowedModelValueTypes: [Array, Object, Number, Boolean, String],
|
||||
suffix: '',
|
||||
...customConfig,
|
||||
};
|
||||
|
||||
describe(`InteractionStateMixin ${cfg.suffix ? `(${cfg.suffix})` : ''}`, async () => {
|
||||
let tag;
|
||||
before(() => {
|
||||
if (!cfg.tagString) {
|
||||
cfg.tagString = defineCE(
|
||||
class IState extends InteractionStateMixin(LitElement) {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.tabIndex = 0;
|
||||
}
|
||||
|
||||
set modelValue(v) {
|
||||
this._modelValue = v;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('model-value-changed', { bubbles: true, composed: true }),
|
||||
);
|
||||
}
|
||||
|
||||
get modelValue() {
|
||||
return this._modelValue;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
tag = unsafeStatic(cfg.tagString);
|
||||
});
|
||||
|
||||
it('sets states to false on init', async () => {
|
||||
const el = await fixture(html`<${tag}></${tag}>`);
|
||||
expect(el.dirty).to.be.false;
|
||||
expect(el.touched).to.be.false;
|
||||
expect(el.prefilled).to.be.false;
|
||||
});
|
||||
|
||||
it('sets dirty when value changed', async () => {
|
||||
const el = await fixture(html`<${tag}></${tag}>`);
|
||||
expect(el.dirty).to.be.false;
|
||||
el.modelValue = 'foobar';
|
||||
expect(el.dirty).to.be.true;
|
||||
});
|
||||
|
||||
it('sets touched to true when field left after focus', async () => {
|
||||
const el = await fixture(html`<${tag}></${tag}>`);
|
||||
await triggerFocusFor(el);
|
||||
await triggerBlurFor(el);
|
||||
expect(el.touched).to.be.true;
|
||||
});
|
||||
|
||||
// classes are added only for backward compatibility - they are deprecated
|
||||
it('sets a class "state-(touched|dirty)"', async () => {
|
||||
const el = await fixture(html`<${tag}></${tag}>`);
|
||||
el.touched = true;
|
||||
await el.updateComplete;
|
||||
expect(el.classList.contains('state-touched')).to.equal(true, 'has class "state-touched"');
|
||||
|
||||
el.dirty = true;
|
||||
await el.updateComplete;
|
||||
expect(el.classList.contains('state-dirty')).to.equal(true, 'has class "state-dirty"');
|
||||
});
|
||||
|
||||
it('sets an attribute "touched', async () => {
|
||||
const el = await fixture(html`<${tag}></${tag}>`);
|
||||
el.touched = true;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('touched')).to.be.true;
|
||||
});
|
||||
|
||||
it('sets an attribute "dirty', async () => {
|
||||
const el = await fixture(html`<${tag}></${tag}>`);
|
||||
el.dirty = true;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('dirty')).to.be.true;
|
||||
});
|
||||
|
||||
it('fires "(touched|dirty)-state-changed" event when state changes', async () => {
|
||||
const touchedSpy = sinon.spy();
|
||||
const dirtySpy = sinon.spy();
|
||||
const el = await fixture(
|
||||
html`<${tag} @touched-changed=${touchedSpy} @dirty-changed=${dirtySpy}></${tag}>`,
|
||||
);
|
||||
|
||||
el.touched = true;
|
||||
expect(touchedSpy.callCount).to.equal(1);
|
||||
|
||||
el.dirty = true;
|
||||
expect(dirtySpy.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('sets prefilled once instantiated', async () => {
|
||||
const el = await fixture(html`
|
||||
<${tag} .modelValue=${'prefilled'}></${tag}>
|
||||
`);
|
||||
expect(el.prefilled).to.be.true;
|
||||
|
||||
const nonPrefilled = await fixture(html`
|
||||
<${tag} .modelValue=${''}></${tag}>
|
||||
`);
|
||||
expect(nonPrefilled.prefilled).to.be.false;
|
||||
});
|
||||
|
||||
// This method actually tests the implementation of the _isPrefilled method.
|
||||
it(`can determine "prefilled" based on different modelValue types
|
||||
(${cfg.allowedModelValueTypes.map(t => t.name).join(', ')})`, async () => {
|
||||
const el = await fixture(html`<${tag}></${tag}>`);
|
||||
|
||||
const changeModelValueAndLeave = modelValue => {
|
||||
const targetEl = el.inputElement || el;
|
||||
targetEl.dispatchEvent(new Event('focus', { bubbles: true }));
|
||||
el.modelValue = modelValue;
|
||||
targetEl.dispatchEvent(new Event(el._leaveEvent, { bubbles: true }));
|
||||
};
|
||||
|
||||
// Prefilled
|
||||
if (cfg.allowedModelValueTypes.includes(Array)) {
|
||||
changeModelValueAndLeave(['not-empty']);
|
||||
expect(el.prefilled, 'not empty array should be "prefilled"').to.be.true;
|
||||
changeModelValueAndLeave([]);
|
||||
expect(el.prefilled, 'empty array should not be "prefilled"').to.be.false;
|
||||
}
|
||||
if (cfg.allowedModelValueTypes.includes(Object)) {
|
||||
changeModelValueAndLeave({ not: 'empty' });
|
||||
expect(el.prefilled, 'not empty object should be "prefilled"').to.be.true;
|
||||
changeModelValueAndLeave({});
|
||||
expect(el.prefilled, 'empty object should not be "prefilled"').to.be.false;
|
||||
}
|
||||
if (cfg.allowedModelValueTypes.includes(Number)) {
|
||||
changeModelValueAndLeave(0);
|
||||
expect(el.prefilled, 'numbers should be "prefilled"').to.be.true;
|
||||
}
|
||||
if (cfg.allowedModelValueTypes.includes(String)) {
|
||||
changeModelValueAndLeave(false);
|
||||
expect(el.prefilled, 'booleans should be "prefilled"').to.be.true;
|
||||
changeModelValueAndLeave('');
|
||||
expect(el.prefilled, 'empty string should not be "prefilled"').to.be.false;
|
||||
}
|
||||
|
||||
// Not prefilled
|
||||
changeModelValueAndLeave(null);
|
||||
expect(el.prefilled, 'null should not be "prefilled"').to.be.false;
|
||||
changeModelValueAndLeave(undefined);
|
||||
expect(el.prefilled, 'undefined should not be "prefilled"').to.be.false;
|
||||
});
|
||||
|
||||
it('has a method resetInteractionState()', async () => {
|
||||
const el = await fixture(html`<${tag}></${tag}>`);
|
||||
el.dirty = true;
|
||||
el.touched = true;
|
||||
el.prefilled = true;
|
||||
el.resetInteractionState();
|
||||
expect(el.dirty).to.be.false;
|
||||
expect(el.touched).to.be.false;
|
||||
expect(el.prefilled).to.be.false;
|
||||
|
||||
el.dirty = true;
|
||||
el.touched = true;
|
||||
el.prefilled = false;
|
||||
el.modelValue = 'Some value';
|
||||
el.resetInteractionState();
|
||||
expect(el.dirty).to.be.false;
|
||||
expect(el.touched).to.be.false;
|
||||
expect(el.prefilled).to.be.true;
|
||||
});
|
||||
|
||||
describe('SubClassers', () => {
|
||||
it('can override the `_leaveEvent`', async () => {
|
||||
const tagLeaveString = defineCE(
|
||||
class IState extends InteractionStateMixin(LitElement) {
|
||||
constructor() {
|
||||
super();
|
||||
this._leaveEvent = 'custom-blur';
|
||||
}
|
||||
},
|
||||
);
|
||||
const tagLeave = unsafeStatic(tagLeaveString);
|
||||
const el = await fixture(html`<${tagLeave}></${tagLeave}>`);
|
||||
el.dispatchEvent(new Event('custom-blur'));
|
||||
expect(el.touched).to.be.true;
|
||||
});
|
||||
|
||||
it('can override the deprecated `leaveEvent`', async () => {
|
||||
const el = await fixture(html`<${tag} .leaveEvent=${'custom-blur'}></${tag}>`);
|
||||
expect(el._leaveEvent).to.equal('custom-blur');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,189 +1,3 @@
|
|||
import {
|
||||
expect,
|
||||
fixture,
|
||||
unsafeStatic,
|
||||
html,
|
||||
defineCE,
|
||||
triggerFocusFor,
|
||||
triggerBlurFor,
|
||||
} from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import { LitElement } from '@lion/core';
|
||||
import { runInteractionStateMixinSuite } from '../test-suites/InteractionStateMixin.suite.js';
|
||||
|
||||
import { InteractionStateMixin } from '../src/InteractionStateMixin.js';
|
||||
|
||||
describe('InteractionStateMixin', async () => {
|
||||
let tagString;
|
||||
let tag;
|
||||
before(() => {
|
||||
tagString = defineCE(
|
||||
class IState extends InteractionStateMixin(LitElement) {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.tabIndex = 0;
|
||||
}
|
||||
|
||||
set modelValue(v) {
|
||||
this._modelValue = v;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('model-value-changed', { bubbles: true, composed: true }),
|
||||
);
|
||||
}
|
||||
|
||||
get modelValue() {
|
||||
return this._modelValue;
|
||||
}
|
||||
},
|
||||
);
|
||||
tag = unsafeStatic(tagString);
|
||||
});
|
||||
|
||||
it('sets states to false on init', async () => {
|
||||
const el = await fixture(html`<${tag}></${tag}>`);
|
||||
expect(el.dirty).to.be.false;
|
||||
expect(el.touched).to.be.false;
|
||||
expect(el.prefilled).to.be.false;
|
||||
});
|
||||
|
||||
it('sets dirty when value changed', async () => {
|
||||
const el = await fixture(html`<${tag}></${tag}>`);
|
||||
expect(el.dirty).to.be.false;
|
||||
el.modelValue = 'foobar';
|
||||
expect(el.dirty).to.be.true;
|
||||
});
|
||||
|
||||
it('sets touched to true when field left after focus', async () => {
|
||||
const el = await fixture(html`<${tag}></${tag}>`);
|
||||
await triggerFocusFor(el);
|
||||
await triggerBlurFor(el);
|
||||
expect(el.touched).to.be.true;
|
||||
});
|
||||
|
||||
// classes are added only for backward compatibility - they are deprecated
|
||||
it('sets a class "state-(touched|dirty)"', async () => {
|
||||
const el = await fixture(html`<${tag}></${tag}>`);
|
||||
el.touched = true;
|
||||
await el.updateComplete;
|
||||
expect(el.classList.contains('state-touched')).to.equal(true, 'has class "state-touched"');
|
||||
|
||||
el.dirty = true;
|
||||
await el.updateComplete;
|
||||
expect(el.classList.contains('state-dirty')).to.equal(true, 'has class "state-dirty"');
|
||||
});
|
||||
|
||||
it('sets an attribute "touched', async () => {
|
||||
const el = await fixture(html`<${tag}></${tag}>`);
|
||||
el.touched = true;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('touched')).to.be.true;
|
||||
});
|
||||
|
||||
it('sets an attribute "dirty', async () => {
|
||||
const el = await fixture(html`<${tag}></${tag}>`);
|
||||
el.dirty = true;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('dirty')).to.be.true;
|
||||
});
|
||||
|
||||
it('fires "(touched|dirty)-state-changed" event when state changes', async () => {
|
||||
const touchedSpy = sinon.spy();
|
||||
const dirtySpy = sinon.spy();
|
||||
const el = await fixture(
|
||||
html`<${tag} @touched-changed=${touchedSpy} @dirty-changed=${dirtySpy}></${tag}>`,
|
||||
);
|
||||
|
||||
el.touched = true;
|
||||
expect(touchedSpy.callCount).to.equal(1);
|
||||
|
||||
el.dirty = true;
|
||||
expect(dirtySpy.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('sets prefilled once instantiated', async () => {
|
||||
const el = await fixture(html`
|
||||
<${tag} .modelValue=${'prefilled'}></${tag}>
|
||||
`);
|
||||
expect(el.prefilled).to.be.true;
|
||||
|
||||
const nonPrefilled = await fixture(html`
|
||||
<${tag} .modelValue=${''}></${tag}>
|
||||
`);
|
||||
expect(nonPrefilled.prefilled).to.be.false;
|
||||
});
|
||||
|
||||
// This method actually tests the implementation of the _isPrefilled method.
|
||||
it(`can determine "prefilled" based on different modelValue types (Arrays, Objects, Numbers,
|
||||
Booleans, Strings)`, async () => {
|
||||
const el = await fixture(html`<${tag}></${tag}>`);
|
||||
|
||||
const changeModelValueAndLeave = modelValue => {
|
||||
el.dispatchEvent(new Event('focus', { bubbles: true }));
|
||||
el.modelValue = modelValue;
|
||||
el.dispatchEvent(new Event('blur', { bubbles: true }));
|
||||
};
|
||||
|
||||
// Prefilled
|
||||
changeModelValueAndLeave(['not-empty']);
|
||||
expect(el.prefilled, 'not empty array should be "prefilled"').to.be.true;
|
||||
changeModelValueAndLeave({ not: 'empty' });
|
||||
expect(el.prefilled, 'not empty object should be "prefilled"').to.be.true;
|
||||
changeModelValueAndLeave(0);
|
||||
expect(el.prefilled, 'numbers should be "prefilled"').to.be.true;
|
||||
changeModelValueAndLeave(false);
|
||||
expect(el.prefilled, 'booleans should be "prefilled"').to.be.true;
|
||||
|
||||
// Not prefilled
|
||||
changeModelValueAndLeave([]);
|
||||
expect(el.prefilled, 'empty array should not be "prefilled"').to.be.false;
|
||||
changeModelValueAndLeave({});
|
||||
expect(el.prefilled, 'empty object should not be "prefilled"').to.be.false;
|
||||
changeModelValueAndLeave('');
|
||||
expect(el.prefilled, 'empty string should not be "prefilled"').to.be.false;
|
||||
changeModelValueAndLeave(null);
|
||||
expect(el.prefilled, 'null should not be "prefilled"').to.be.false;
|
||||
changeModelValueAndLeave(undefined);
|
||||
expect(el.prefilled, 'undefined should not be "prefilled"').to.be.false;
|
||||
});
|
||||
|
||||
it('has a method resetInteractionState()', async () => {
|
||||
const el = await fixture(html`<${tag}></${tag}>`);
|
||||
el.dirty = true;
|
||||
el.touched = true;
|
||||
el.prefilled = true;
|
||||
el.resetInteractionState();
|
||||
expect(el.dirty).to.be.false;
|
||||
expect(el.touched).to.be.false;
|
||||
expect(el.prefilled).to.be.false;
|
||||
|
||||
el.dirty = true;
|
||||
el.touched = true;
|
||||
el.prefilled = false;
|
||||
el.modelValue = 'Some value';
|
||||
el.resetInteractionState();
|
||||
expect(el.dirty).to.be.false;
|
||||
expect(el.touched).to.be.false;
|
||||
expect(el.prefilled).to.be.true;
|
||||
});
|
||||
|
||||
describe('SubClassers', () => {
|
||||
it('can override the `_leaveEvent`', async () => {
|
||||
const tagLeaveString = defineCE(
|
||||
class IState extends InteractionStateMixin(LitElement) {
|
||||
constructor() {
|
||||
super();
|
||||
this._leaveEvent = 'custom-blur';
|
||||
}
|
||||
},
|
||||
);
|
||||
const tagLeave = unsafeStatic(tagLeaveString);
|
||||
const el = await fixture(html`<${tagLeave}></${tagLeave}>`);
|
||||
el.dispatchEvent(new Event('custom-blur'));
|
||||
expect(el.touched).to.be.true;
|
||||
});
|
||||
|
||||
it('can override the deprecated `leaveEvent`', async () => {
|
||||
const el = await fixture(html`<${tag} .leaveEvent=${'custom-blur'}></${tag}>`);
|
||||
expect(el._leaveEvent).to.equal('custom-blur');
|
||||
});
|
||||
});
|
||||
});
|
||||
runInteractionStateMixinSuite();
|
||||
|
|
|
|||
22
packages/field/test/field-integrations.test.js
Normal file
22
packages/field/test/field-integrations.test.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { defineCE } from '@open-wc/testing';
|
||||
import { runInteractionStateMixinSuite } from '../test-suites/InteractionStateMixin.suite.js';
|
||||
import '../lion-field.js';
|
||||
|
||||
const fieldTagString = defineCE(
|
||||
class extends customElements.get('lion-field') {
|
||||
get slots() {
|
||||
return {
|
||||
...super.slots,
|
||||
// LionField needs to have an inputElement defined in order to work...
|
||||
input: () => document.createElement('input'),
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
describe('<lion-field> integrations', () => {
|
||||
runInteractionStateMixinSuite({
|
||||
tagString: fieldTagString,
|
||||
suffix: 'lion-field',
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue