lion/packages/validate/test-suites/FeedbackMixin.suite.js
Thijs Louisse 6e81b55e3c feat(validate): new validation api, async validation and more
Co-authored-by: Thomas Allmer <Thomas.Allmer@ing.com>
2019-11-15 16:57:58 +01:00

344 lines
11 KiB
JavaScript

/* eslint-disable max-classes-per-file, no-param-reassign, no-unused-expressions */
// eslint-disable-next-line import/no-extraneous-dependencies
import { expect, fixture, html, unsafeStatic, defineCE } from '@open-wc/testing';
// eslint-disable-next-line import/no-extraneous-dependencies
import sinon from 'sinon';
import { LitElement } from '@lion/core';
import { ValidateMixin } from '../src/ValidateMixin.js';
import { Validator } from '../src/Validator.js';
import { Required } from '../src/validators/Required.js';
import { MinLength } from '../src/validators/StringValidators.js';
import { DefaultSuccess } from '../src/resultValidators/DefaultSuccess.js';
import { AlwaysInvalid } from '../test-helpers/helper-validators.js';
import '../lion-validation-feedback.js';
import { FeedbackMixin } from '../src/FeedbackMixin.js';
export function runFeedbackMixinSuite(customConfig) {
const cfg = {
tagString: null,
...customConfig,
};
const lightDom = cfg.lightDom || '';
describe('Validity Feedback', () => {
afterEach(() => {
sinon.restore();
});
// eslint-disable-next-line no-shadow
const tagString = defineCE(
class extends FeedbackMixin(ValidateMixin(LitElement)) {
static get properties() {
return { modelValue: String };
}
connectedCallback() {
super.connectedCallback();
this.appendChild(document.createElement('input'));
}
get _inputNode() {
return this.querySelector('input');
}
},
);
// eslint-disable-next-line no-shadow
const tag = unsafeStatic(tagString);
class ContainsLowercaseA extends Validator {
constructor(...args) {
super(...args);
this.name = 'ContainsLowercaseA';
this.execute = modelValue => !modelValue.includes('a');
}
}
class ContainsCat extends Validator {
constructor(...args) {
super(...args);
this.name = 'containsCat';
this.execute = modelValue => !modelValue.includes('cat');
}
}
AlwaysInvalid.getMessage = () => 'Message for AlwaysInvalid';
MinLength.getMessage = () => 'Message for MinLength';
ContainsLowercaseA.getMessage = () => 'Message for ContainsLowercaseA';
ContainsCat.getMessage = () => 'Message for ContainsCat';
it('sets ".hasErrorVisible"/[has-error-visible] when visibility condition is met', async () => {
const el = await fixture(html`
<${tag} .validators=${[new MinLength(3)]}>${lightDom}</${tag}>`);
if (cfg.enableFeedbackVisible) {
cfg.enableFeedbackVisible(el);
}
el.modelValue = 'a';
await el.feedbackComplete;
expect(el.hasErrorVisible).to.be.true;
expect(el.hasAttribute('has-error-visible')).to.be.true;
el.modelValue = 'abc';
await el.feedbackComplete;
expect(el.hasErrorVisible).to.be.false;
expect(el.hasAttribute('has-error-visible')).to.be.false;
});
it('passes a message to the "._feedbackNode"', async () => {
const el = await fixture(html`
<${tag}
.modelValue=${'cat'}
>${lightDom}</${tag}>
`);
expect(el._feedbackNode.feedbackData).to.be.undefined;
el.validators = [new AlwaysInvalid()];
await el.feedbackComplete;
expect(el._feedbackNode.feedbackData[0].message).to.equal('Message for AlwaysInvalid');
});
it('has configurable feedback visibility hook', async () => {
const el = await fixture(html`
<${tag}
.modelValue=${'cat'}
.validators=${[new AlwaysInvalid()]}
>${lightDom}</${tag}>
`);
await el.feedbackComplete;
expect(el._feedbackNode.feedbackData[0].message).to.equal('Message for AlwaysInvalid');
el._prioritizeAndFilterFeedback = () => []; // filter out all errors
await el.validate();
await el.feedbackComplete;
expect(el._feedbackNode.feedbackData).to.be.undefined;
});
it('writes prioritized result to "._feedbackNode" based on Validator order', async () => {
const el = await fixture(html`
<${tag}
.modelValue=${'cat'}
.validators=${[new AlwaysInvalid(), new MinLength(4)]}
>${lightDom}</${tag}>
`);
await el.feedbackComplete;
expect(el._feedbackNode.feedbackData[0].message).to.equal('Message for AlwaysInvalid');
});
it('renders validation result to "._feedbackNode" when async messages are resolved', async () => {
let unlockMessage;
const messagePromise = new Promise(resolve => {
unlockMessage = resolve;
});
AlwaysInvalid.getMessage = async () => {
await messagePromise;
return 'this ends up in "._feedbackNode"';
};
const el = await fixture(html`
<${tag}
.modelValue=${'cat'}
.validators=${[new AlwaysInvalid()]}
>${lightDom}</${tag}>
`);
expect(el._feedbackNode.feedbackData).to.be.undefined;
unlockMessage();
await el.feedbackComplete;
expect(el._feedbackNode.feedbackData[0].message).to.equal('this ends up in "._feedbackNode"');
});
// N.B. this replaces the 'config.hideFeedback' option we had before...
it('renders empty result when Validator.getMessage() returns "null"', async () => {
let unlockMessage;
const messagePromise = new Promise(resolve => {
unlockMessage = resolve;
});
AlwaysInvalid.getMessage = async () => {
await messagePromise;
return 'this ends up in "._feedbackNode"';
};
const el = await fixture(html`
<${tag}
.modelValue=${'cat'}
.validators=${[new AlwaysInvalid()]}
>${lightDom}</${tag}>
`);
expect(el._feedbackNode.feedbackData).to.be.undefined;
unlockMessage();
await el.feedbackComplete;
expect(el._feedbackNode.feedbackData[0].message).to.equal('this ends up in "._feedbackNode"');
});
it('supports custom element to render feedback', async () => {
const customFeedbackTagString = defineCE(
class extends LitElement {
static get properties() {
return {
feedbackData: Array,
};
}
render() {
return html`
Custom for ${this.feedbackData[0].validator.name}
`;
}
},
);
const customFeedbackTag = unsafeStatic(customFeedbackTagString);
const el = await fixture(html`
<${tag}
.validators=${[new ContainsLowercaseA(), new AlwaysInvalid()]}>
<${customFeedbackTag} slot="feedback"><${customFeedbackTag}>
</${tag}>
`);
expect(el._feedbackNode.localName).to.equal(customFeedbackTagString);
el.modelValue = 'dog';
await el.feedbackComplete;
await el._feedbackNode.updateComplete;
expect(el._feedbackNode).shadowDom.to.equal('Custom for ContainsLowercaseA');
el.modelValue = 'cat';
await el.feedbackComplete;
await el._feedbackNode.updateComplete;
expect(el._feedbackNode).shadowDom.to.equal('Custom for AlwaysInvalid');
});
it('supports custom messages in Validator instance configuration object', async () => {
const el = await fixture(html`
<${tag} .validators=${[new MinLength(3, { getMessage: () => 'custom via config' })]}
>${lightDom}</${tag}>
`);
el.modelValue = 'a';
await el.feedbackComplete;
expect(el._feedbackNode.feedbackData[0].message).to.equal('custom via config');
});
it('shows success message after fixing an error', async () => {
const el = await fixture(html`
<${tag}
.validators=${[
new MinLength(3),
new DefaultSuccess(null, { getMessage: () => 'This is a success message' }),
]}
>${lightDom}</${tag}>
`);
el.modelValue = 'a';
await el.feedbackComplete;
expect(el._feedbackNode.feedbackData[0].message).to.equal('Message for MinLength');
el.modelValue = 'abcd';
await el.feedbackComplete;
expect(el._feedbackNode.feedbackData[0].message).to.equal('This is a success message');
});
describe('Accessibility', () => {
it('sets [aria-invalid="true"] to "._inputNode" when ".hasError" is true', async () => {
const el = await fixture(html`
<${tag}
.validators=${[new Required()]}
.modelValue=${'a'}
>${lightDom}</${tag}>
`);
const inputNode = el._inputNode;
expect(inputNode.getAttribute('aria-invalid')).to.equal('false');
el.modelValue = '';
await el.feedbackComplete;
expect(inputNode.getAttribute('aria-invalid')).to.equal('true');
el.modelValue = 'a';
await el.feedbackComplete;
expect(inputNode.getAttribute('aria-invalid')).to.equal('false');
});
});
describe('Meta data', () => {
it('".getMessage()" gets a reference to formControl, validatorParams and modelValue', async () => {
let el;
const constructorValidator = new MinLength(4, { type: 'x' }); // type to prevent duplicates
const constructorMessageSpy = sinon.spy(constructorValidator.constructor, 'getMessage');
el = await fixture(html`
<${tag}
.validators=${[constructorValidator]}
.modelValue=${'cat'}
>${lightDom}</${tag}>
`);
await el.feedbackComplete;
expect(constructorMessageSpy.args[0][0]).to.eql({
validatorParams: 4,
modelValue: 'cat',
formControl: el,
fieldName: undefined,
});
const instanceMessageSpy = sinon.spy();
const instanceValidator = new MinLength(4, { getMessage: instanceMessageSpy });
el = await fixture(html`
<${tag}
.validators=${[instanceValidator]}
.modelValue=${'cat'}
>${lightDom}</${tag}>
`);
await el.feedbackComplete;
expect(instanceMessageSpy.args[0][0]).to.eql({
validatorParams: 4,
modelValue: 'cat',
formControl: el,
fieldName: undefined,
});
});
it('".getMessage()" gets .fieldName defined on instance', async () => {
const constructorValidator = new MinLength(4, { type: 'x' }); // type to prevent duplicates
const spy = sinon.spy(constructorValidator.constructor, 'getMessage');
const el = await fixture(html`
<${tag}
.validators=${[constructorValidator]}
.modelValue=${'cat'}
.fieldName=${new Promise(resolve => resolve('myField'))}
>${lightDom}</${tag}>
`);
await el.feedbackComplete;
expect(spy.args[0][0]).to.eql({
validatorParams: 4,
modelValue: 'cat',
formControl: el,
fieldName: 'myField',
});
});
});
it('".getMessage()" gets .fieldName defined on Validator config', async () => {
const constructorValidator = new MinLength(4, {
fieldName: new Promise(resolve => resolve('myFieldViaCfg')),
});
const spy = sinon.spy(constructorValidator.constructor, 'getMessage');
const el = await fixture(html`
<${tag}
.validators=${[constructorValidator]}
.modelValue=${'cat'}
.fieldName=${new Promise(resolve => resolve('myField'))}
>${lightDom}</${tag}>
`);
await el.feedbackComplete;
expect(spy.args[0][0]).to.eql({
validatorParams: 4,
modelValue: 'cat',
formControl: el,
fieldName: 'myFieldViaCfg',
});
});
});
}