/* eslint-disable no-unused-vars, no-param-reassign */ import { expect, fixture, html, unsafeStatic, defineCE, aTimeout } from '@open-wc/testing'; import sinon from 'sinon'; import { LionLitElement } from '@lion/core/src/LionLitElement.js'; import { localizeTearDown } from '@lion/localize/test-helpers.js'; import { localize } from '@lion/localize'; import { ValidateMixin } from '../src/ValidateMixin.js'; import { Unparseable } from '../src/Unparseable.js'; // element, lightDom, errorShowPrerequisite, warningShowPrerequisite, infoShowPrerequisite, // successShowPrerequisite const externalVariables = {}; const suffixName = ''; const lightDom = ''; const tagString = defineCE( class extends ValidateMixin(LionLitElement) { static get properties() { return { ...super.properties, modelValue: { type: String, }, }; } }, ); const defaultRequiredFn = modelValue => ({ required: modelValue !== '' }); const tag = unsafeStatic(tagString); beforeEach(() => { localizeTearDown(); }); describe('ValidateMixin', () => { it('supports multiple validator types: error, warning, info and success [spec-to-be-implemented]', () => { // TODO: implement spec }); /** * Terminology * * - *validatable-field* * The element ('this') the ValidateMixin is applied on. * * - *input-element* * The 'this.inputElement' property (usually a getter) that returns/contains a reference to an * interaction element that receives focus, displays the input value, interaction states are * derived from, aria properties are put on and setCustomValidity (if applicable) is called on. * Can be input, textarea, my-custom-slider etc. * * - *feedback-element* * The 'this._messageElement' property (usually a getter) that returns/contains a reference to * the output container for validation feedback. Messages will be written to this element * based on user defined or default validity feedback visibility conditions. * * - *show-{type}-feedback-condition* * The 'this.show-{'error'|'warning'|'info'|'success'}' value that stores whether the * feedback for the particular validation type should be shown to the end user. */ it('validates immediately (once form field has bootstrapped/initialized)', async () => { function alwaysFalse() { return { alwaysFalse: false }; } const el = await fixture(html` <${tag} .errorValidators=${[[alwaysFalse]]} .warningValidators=${[[alwaysFalse]]} .infoValidators=${[[alwaysFalse]]} .successValidators=${[[alwaysFalse]]} .modelValue=${'trigger initial validation'} >${lightDom} `); expect(el.errorState).to.equal(true); expect(el.warningState).to.equal(true); expect(el.infoState).to.equal(true); expect(el.successState).to.equal(true); }); it('revalidates when value changes', async () => { function alwaysTrue() { return { alwaysTrue: true }; } const el = await fixture(html` <${tag} .errorValidators=${[[alwaysTrue]]} .modelValue=${'myValue'} >${lightDom} `); const validateSpy = sinon.spy(el, 'validate'); el.modelValue = 'x'; expect(validateSpy.callCount).to.equal(1); }); // TODO: fix and renable it.skip('reconsiders feedback visibility when interaction states changed', async () => { // see https://codeburst.io/javascript-async-await-with-foreach-b6ba62bbf404 async function asyncForEach(array, callback) { for (let i = 0; i < array.length; i += 1) { // we explicitly want to run it one after each other await callback(array[i], i, array); // eslint-disable-line no-await-in-loop } } const el = await fixture(html` <${tag} .errorValidators=${[ [ () => { true; }, ], ]} .modelValue=${'myValue'} >${lightDom} `); const messageSpy = sinon.spy(el, '_createMessageAndRenderFeedback'); await asyncForEach(['dirty', 'touched', 'prefilled', 'submitted'], async (state, i) => { el[state] = false; await el.updateComplete; el[state] = true; await el.updateComplete; expect(messageSpy.callCount).to.equal(i + 1); }); }); it('works with viewValue when input is not parseable', async () => { const otherValidatorSpy = sinon.spy(value => ({ otherValidator: false })); await fixture(html` <${tag} .errorValidators=${[['required'], [otherValidatorSpy]]} .__isRequired=${defaultRequiredFn} .modelValue=${new Unparseable('foo')} >${lightDom} `); expect(otherValidatorSpy.calledWith('foo')).to.equal(true); }); describe(`Validators ${suffixName}`, () => { function isCat(modelValue, opts) { const validateString = opts && opts.number ? `cat${opts.number}` : 'cat'; return { isCat: modelValue === validateString }; } it('is plain js function returning { validatorName: true/false }', async () => { const el = await fixture(html` <${tag} .modelValue=${'cat'} .errorValidators=${[[isCat]]} >${lightDom} `); expect(typeof el.errorValidators[0][0]).to.equal('function'); expect(el.errorValidators[0][0]('cat').isCat).to.equal(true); expect(el.errorValidators[0][0]('dog').isCat).to.equal(false); }); it('accepts additional parameters as a second argument (e.g. options)', async () => { expect(isCat('cat').isCat).to.equal(true); expect(isCat('cat', { number: 5 }).isCat).to.equal(false); expect(isCat('cat5', { number: 5 }).isCat).to.equal(true); const el = await fixture(html` <${tag} .modelValue=${'cat'} .errorValidators=${[[isCat, { number: 5 }]]} >${lightDom} `); expect(el.errorValidators[0][1]).to.deep.equal({ number: 5 }); expect(el.errorState).to.equal(true); el.errorValidators = [[isCat]]; el.modelValue = 'cat'; expect(el.errorState).to.equal(false); }); it('will not trigger on empty values', async () => { const el = await fixture(html` <${tag} .errorValidators=${[[isCat]]} >${lightDom} `); el.modelValue = 'cat'; expect(el.error.isCat).to.be.undefined; el.modelValue = 'dog'; expect(el.error.isCat).to.be.true; el.modelValue = ''; expect(el.error.isCat).to.be.undefined; }); it('gets retriggered on parameter change [to-be-investigated]', async () => { // TODO: find way to technically implement this, // e.g. define validator params as props on , just like native validators }); it(`replaces native validators (required, minlength, maxlength, min, max, pattern, step, type(email/date/number/...) etc.) [to-be-investigated]`, async () => { // TODO: this could also become: "can be used in conjunction with" }); it('can have multiple validators per type', async () => { function containsLowercaseA(modelValue) { return { containsLowercaseA: modelValue.indexOf('a') > -1 }; } const spyIsCat = sinon.spy(isCat); const spyContainsLowercaseA = sinon.spy(containsLowercaseA); const multipleValidators = await fixture(html` <${tag} .label=${'myField'} .modelValue=${'cat'} .errorValidators=${[[spyIsCat], [spyContainsLowercaseA]]} >${lightDom} `); expect(multipleValidators.errorValidators.length).to.equal(2); expect(spyIsCat.callCount).to.equal(1); expect(spyContainsLowercaseA.callCount).to.equal(1); expect(multipleValidators.errorState).to.equal(false); multipleValidators.modelValue = 'dog'; expect(spyIsCat.callCount).to.equal(2); expect(spyContainsLowercaseA.callCount).to.equal(2); expect(multipleValidators.errorState).to.equal(true); }); }); describe(`Required validator`, () => { it('has a string notation', async () => { const el = await fixture(html` <${tag} .errorValidators=${[['required']]} .__isRequired="${defaultRequiredFn}" .modelValue=${'foo'} >${lightDom} `); const requiredValidatorSpy = sinon.spy(el, '__isRequired'); el.modelValue = ''; expect(requiredValidatorSpy.callCount).to.equal(1); expect(el.error.required).to.equal(true); }); it('can have different implementations for different form controls', async () => { const el = await fixture(html` <${tag} .errorValidators=${[['required']]} .__isRequired=${modelValue => ({ required: modelValue.model !== '' })} .modelValue=${{ model: 'foo' }} >${lightDom} `); const requiredValidatorSpy = sinon.spy(el, '__isRequired'); el.modelValue = { model: '' }; expect(requiredValidatorSpy.callCount).to.equal(1); expect(el.error.required).to.equal(true); }); it('prevents other validators from being called when required validator returns false', async () => { const alwaysFalseSpy = sinon.spy(() => ({ alwaysFalse: false })); const el = await fixture(html` <${tag} .errorValidators=${[['required'], [alwaysFalseSpy]]} .__isRequired=${defaultRequiredFn} .modelValue=${''} >${lightDom} `); expect(alwaysFalseSpy.callCount).to.equal(0); // __isRequired returned false (invalid) el.modelValue = 'foo'; expect(alwaysFalseSpy.callCount).to.equal(1); // __isRequired returned true (valid) }); }); describe(`Element Validation States ${suffixName}`, () => { const alwaysFalse = () => ({ alwaysFalse: false }); const minLength = (modelValue, { min }) => ({ minLength: modelValue.length >= min }); const containsLowercaseA = modelValue => ({ containsLowercaseA: modelValue.indexOf('a') > -1 }); const containsLowercaseB = modelValue => ({ containsLowercaseB: modelValue.indexOf('b') > -1 }); it('stores current state of every type in this.(error|warning|info|success)State', async () => { const validationState = await fixture(html` <${tag} .errorValidators=${[[minLength, { min: 3 }]]} .warningValidators=${[[minLength, { min: 5 }]]} .infoValidators=${[[minLength, { min: 7 }]]} .successValidators=${[[alwaysFalse]]} >${lightDom} `); validationState.modelValue = 'a'; expect(validationState.errorState).to.equal(true); expect(validationState.warningState).to.equal(true); expect(validationState.infoState).to.equal(true); expect(validationState.successState).to.equal(true); validationState.modelValue = 'abc'; expect(validationState.errorState).to.equal(false); expect(validationState.warningState).to.equal(true); expect(validationState.infoState).to.equal(true); expect(validationState.successState).to.equal(true); validationState.modelValue = 'abcde'; expect(validationState.errorState).to.equal(false); expect(validationState.warningState).to.equal(false); expect(validationState.infoState).to.equal(true); expect(validationState.successState).to.equal(true); validationState.modelValue = 'abcdefg'; expect(validationState.errorState).to.equal(false); expect(validationState.warningState).to.equal(false); expect(validationState.infoState).to.equal(false); expect(validationState.successState).to.equal(true); }); it('fires "(error|warning|info|success)-changed" event when {state} changes', async () => { const validationState = await fixture(html` <${tag} .errorValidators=${[[minLength, { min: 3 }], [containsLowercaseA], [containsLowercaseB]]} >${lightDom} `); const cbError = sinon.spy(); validationState.addEventListener('error-changed', cbError); validationState.modelValue = 'a'; expect(cbError.callCount).to.equal(1); validationState.modelValue = 'aa'; expect(cbError.callCount).to.equal(1); validationState.modelValue = 'aaa'; expect(cbError.callCount).to.equal(2); validationState.modelValue = 'aba'; expect(cbError.callCount).to.equal(3); }); it(`sets a class "state-(error|warning|info|success)" when the component has a corresponding state`, async () => { const element = await fixture(html` <${tag} .errorValidators=${[[minLength, { min: 3 }]]} .warningValidators=${[[minLength, { min: 5 }]]} .infoValidators=${[[minLength, { min: 7 }]]} .successValidators=${[[alwaysFalse]]} >${lightDom}`); element.modelValue = 'a'; await element.updateComplete; expect(element.classList.contains('state-error')).to.equal(true, 'has state-error'); expect(element.classList.contains('state-warning')).to.equal(true, 'has state-warning'); expect(element.classList.contains('state-info')).to.equal(true, 'has state-info'); expect(element.classList.contains('state-success')).to.equal(true, 'has state-success'); element.modelValue = 'abc'; await element.updateComplete; expect(element.classList.contains('state-error')).to.equal(false, 'has no state-error'); expect(element.classList.contains('state-warning')).to.equal(true, 'has state-warning'); expect(element.classList.contains('state-info')).to.equal(true, 'has state-info'); expect(element.classList.contains('state-success')).to.equal(true, 'has state-success'); element.modelValue = 'abcde'; await element.updateComplete; expect(element.classList.contains('state-error')).to.equal(false, 'has no state-error'); expect(element.classList.contains('state-warning')).to.equal(false, 'has no state-warning'); expect(element.classList.contains('state-info')).to.equal(true, 'has state-info'); expect(element.classList.contains('state-success')).to.equal(true, 'has state-success'); element.modelValue = 'abcdefg'; await element.updateComplete; expect(element.classList.contains('state-error')).to.equal(false, 'has no state-error'); expect(element.classList.contains('state-warning')).to.equal(false, 'has no state-warning'); expect(element.classList.contains('state-info')).to.equal(false, 'has no state-info'); expect(element.classList.contains('state-success')).to.equal(true, 'has state-success'); }); it(`stores validity of validator for every type in this.(error|warning|info|success).{validatorName}`, async () => { const validationState = await fixture(html` <${tag} .modelValue=${'a'} .errorValidators=${[[minLength, { min: 3 }], [alwaysFalse]]} >${lightDom}`); expect(validationState.error.minLength).to.equal(true); expect(validationState.error.alwaysFalse).to.equal(true); validationState.modelValue = 'abc'; expect(typeof validationState.error.minLength).to.equal('undefined'); expect(validationState.error.alwaysFalse).to.equal(true); }); it(`sets a class "state-(error|warning|info|success)-show" when the component has a corresponding state and "show{type}Condition()" is met`, async () => { const validationState = await fixture(html` <${tag} .errorValidators=${[[minLength, { min: 3 }]]} .warningValidators=${[[minLength, { min: 5 }]]} .infoValidators=${[[minLength, { min: 7 }]]} .successValidators=${[[alwaysFalse]]} >${lightDom}`); if (externalVariables.errorShowPrerequisite) { externalVariables.errorShowPrerequisite(validationState); externalVariables.warningShowPrerequisite(validationState); externalVariables.infoShowPrerequisite(validationState); externalVariables.successShowPrerequisite(validationState); } validationState.modelValue = 'a'; await validationState.updateComplete; expect(validationState.classList.contains('state-error-show')).to.equal(true); expect(validationState.classList.contains('state-warning-show')).to.equal(false); expect(validationState.classList.contains('state-info-show')).to.equal(false); expect(validationState.classList.contains('state-success-show')).to.equal(false); validationState.modelValue = 'abc'; await validationState.updateComplete; expect(validationState.classList.contains('state-error-show')).to.equal(false); expect(validationState.classList.contains('state-warning-show')).to.equal(true); expect(validationState.classList.contains('state-info-show')).to.equal(false); expect(validationState.classList.contains('state-success-show')).to.equal(false); validationState.modelValue = 'abcde'; await validationState.updateComplete; expect(validationState.classList.contains('state-error-show')).to.equal(false); expect(validationState.classList.contains('state-warning-show')).to.equal(false); expect(validationState.classList.contains('state-info-show')).to.equal(true); expect(validationState.classList.contains('state-success-show')).to.equal(false); validationState.modelValue = 'abcdefg'; await validationState.updateComplete; expect(validationState.classList.contains('state-error-show')).to.equal(false); expect(validationState.classList.contains('state-warning-show')).to.equal(false); expect(validationState.classList.contains('state-info-show')).to.equal(false); expect(validationState.classList.contains('state-success-show')).to.equal(false); validationState.modelValue = 'a'; await validationState.updateComplete; expect(validationState.classList.contains('state-error-show')).to.equal(true); expect(validationState.classList.contains('state-warning-show')).to.equal(false); expect(validationState.classList.contains('state-info-show')).to.equal(false); expect(validationState.classList.contains('state-success-show')).to.equal(false); validationState.modelValue = 'abcdefg'; await validationState.updateComplete; expect(validationState.classList.contains('state-error-show')).to.equal(false); expect(validationState.classList.contains('state-warning-show')).to.equal(false); expect(validationState.classList.contains('state-info-show')).to.equal(false); expect(validationState.classList.contains('state-success-show')).to.equal(true); }); it('fires "(error|warning|info|success)-state-changed" event when state changes', async () => { const el = await fixture(html` <${tag} .errorValidators=${[[minLength, { min: 7 }]]} .warningValidators=${[[minLength, { min: 5 }]]} .infoValidators=${[[minLength, { min: 3 }]]} .successValidators=${[[alwaysFalse]]} >${lightDom} `); const cbInfo = sinon.spy(); const cbWarning = sinon.spy(); const cbError = sinon.spy(); const cbSuccess = sinon.spy(); el.addEventListener('error-state-changed', cbError); el.addEventListener('warning-state-changed', cbWarning); el.addEventListener('info-state-changed', cbInfo); el.addEventListener('success-state-changed', cbSuccess); el.modelValue = 'a'; expect(cbError.callCount).to.equal(1); expect(cbWarning.callCount).to.equal(1); expect(cbInfo.callCount).to.equal(1); expect(cbSuccess.callCount).to.equal(1); el.modelValue = 'abc'; expect(cbError.callCount).to.equal(1); expect(cbWarning.callCount).to.equal(1); expect(cbInfo.callCount).to.equal(2); expect(cbSuccess.callCount).to.equal(1); el.modelValue = 'abcde'; expect(cbError.callCount).to.equal(1); expect(cbWarning.callCount).to.equal(2); expect(cbInfo.callCount).to.equal(2); expect(cbSuccess.callCount).to.equal(1); el.modelValue = 'abcdefg'; expect(cbError.callCount).to.equal(2); expect(cbWarning.callCount).to.equal(2); expect(cbInfo.callCount).to.equal(2); expect(cbSuccess.callCount).to.equal(1); }); }); describe(`Accessibility ${suffixName}`, () => { it(`sets property "aria-invalid" to *input-element* once errors should be shown to user(*show-error-feedback-condition* is true) [to-be-implemented]`, async () => {}); it('sets *input-element*.setCustomValidity(errorMessage) [to-be-implemented]', async () => { // TODO: test how and if this affects a11y }); it(`removes validity message from DOM instead of toggling "display:none", to trigger Jaws and VoiceOver [to-be-implemented]`, async () => {}); }); describe(`Validity Feedback ${suffixName}`, () => { function alwaysFalse() { return { alwaysFalse: false }; } function minLength(modelValue, opts) { return { minLength: modelValue.length >= opts.min }; } function containsLowercaseA(modelValue) { return { containsLowercaseA: modelValue.indexOf('a') > -1 }; } function containsCat(modelValue) { return { containsCat: modelValue.indexOf('cat') > -1 }; } const defaultElement = defineCE( class extends ValidateMixin(LionLitElement) { static get properties() { return { ...super.properties, modelValue: { type: String, }, }; } }, ); const defaultElementName = unsafeStatic(defaultElement); beforeEach(() => { // Reset and preload validation translations localizeTearDown(); localize.addData('en-GB', 'lion-validate', { error: { alwaysFalse: 'This is error message for alwaysFalse', minLength: 'This is error message for minLength', containsLowercaseA: 'This is error message for containsLowercaseA', containsCat: 'This is error message for containsCat', }, warning: { alwaysFalse: 'This is warning message for alwaysFalse', minLength: 'This is warning message for minLength', containsLowercaseA: 'This is warning message for containsLowercaseA', containsCat: 'This is warning message for containsCat', }, info: { alwaysFalse: 'This is info message for alwaysFalse', minLength: 'This is info message for minLength', containsLowercaseA: 'This is info message for containsLowercaseA', containsCat: 'This is info message for containsCat', }, success: { alwaysFalse: 'This is success message for alwaysFalse', minLength: 'This is success message for minLength', containsLowercaseA: 'This is success message for containsLowercaseA', containsCat: 'This is success message for containsCat', }, }); }); it('has configurable feedback display condition', async () => { let showErrors = false; const el = await fixture(html` <${tag} .showErrorCondition=${newStates => showErrors && newStates.error} .modelValue=${'cat'} .errorValidators=${[[alwaysFalse]]} >${lightDom} `); expect(el.$$slot('feedback').innerText).to.equal(''); showErrors = true; el.validate(); await el.updateComplete; expect(el.$$slot('feedback').innerText).to.equal('This is error message for alwaysFalse'); }); it('writes validation outcome to *feedback-element*, if present', async () => { const feedbackResult = await fixture(html` <${tag} .modelValue=${'cat'} .errorValidators=${[[alwaysFalse]]} >${lightDom} `); expect(feedbackResult.$$slot('feedback').innerText).to.equal( 'This is error message for alwaysFalse', ); }); it('rerenders validation outcome to *feedback-element*, when dependent on async resources', async () => { const alwaysFalseAsyncTransl = () => ({ alwaysFalseAsyncTransl: false }); const feedbackResult = await fixture(html` <${tag} .modelValue=${'cat'} .errorValidators=${[[alwaysFalseAsyncTransl]]} >${lightDom} `); expect(feedbackResult.$$slot('feedback').innerText).to.equal(''); // locale changed or smth localize.reset(); localize.addData('en-GB', 'lion-validate', { error: { alwaysFalseAsyncTransl: 'error:alwaysFalseAsyncTransl' }, }); feedbackResult.onLocaleUpdated(); expect(feedbackResult.$$slot('feedback').innerText).to.equal('error:alwaysFalseAsyncTransl'); }); it('allows to overwrite the way messages are translated', async () => { const customTranslations = await fixture(html` <${tag} .translateMessage=${(keys, data) => { switch (data.validatorName) { case 'alwaysFalse': return 'You can not pass'; case 'containsLowercaseA': return 'You should have a lowercase a'; default: return ''; } }} .modelValue=${'dog'} .errorValidators=${[[containsLowercaseA], [alwaysFalse]]} >${lightDom} `); expect(customTranslations.$$slot('feedback').innerText).to.equal( 'You should have a lowercase a', ); customTranslations.modelValue = 'cat'; await customTranslations.updateComplete; expect(customTranslations.$$slot('feedback').innerText).to.equal('You can not pass'); }); it('allows to overwrite the way messages are rendered/added to dom', async () => { const element = defineCE( class extends ValidateMixin(LionLitElement) { static get properties() { return { ...super.properties, modelValue: { type: String, }, }; } renderFeedback(validationStates, message) { const validator = message.list[0].data.validatorName; const showError = validationStates.error; this.innerHTML = showError ? `ERROR on ${validator}` : ''; } }, ); const elem = unsafeStatic(element); const ownTranslations = await fixture(html` <${elem} .modelValue=${'dog'} .errorValidators=${[[containsLowercaseA], [alwaysFalse]]} >${lightDom} `); await ownTranslations.updateComplete; expect(ownTranslations.innerHTML).to.equal('ERROR on containsLowercaseA'); ownTranslations.modelValue = 'cat'; await ownTranslations.updateComplete; expect(ownTranslations.innerHTML).to.equal('ERROR on alwaysFalse'); }); it('supports custom element to render feedback', async () => { const errorRenderer = defineCE( class extends HTMLElement { renderFeedback(validationStates, message) { const validator = message.list[0].data.validatorName; const showError = validationStates.error; this.innerText = showError ? `ERROR on ${validator}` : ''; } }, ); const errorRendererName = unsafeStatic(errorRenderer); // TODO: refactor to support integration via externalDependencies.element const element = await fixture(html` <${defaultElementName} .errorValidators=${[[containsLowercaseA], [alwaysFalse]]}> <${errorRendererName} slot="feedback"><${errorRendererName}> `); element.modelValue = 'dog'; await element.updateComplete; expect(element.$$slot('feedback').innerText).to.equal('ERROR on containsLowercaseA'); element.modelValue = 'cat'; await element.updateComplete; expect(element.$$slot('feedback').innerText).to.equal('ERROR on alwaysFalse'); }); it('allows to create a custom feedback renderer via the template [to-be-implemented]', async () => { // TODO: implement }); it('shows only highest priority validation message type (1. error, 2. warning, 3. info)', async () => { // TODO: refactor to support integration via externalDependencies.element const validityFeedback = await fixture(html` <${defaultElementName} .errorValidators=${[[minLength, { min: 3 }]]} .warningValidators=${[[minLength, { min: 5 }]]} .infoValidators=${[[minLength, { min: 7 }]]} .successValidators=${[[alwaysFalse]]} >${lightDom} `); validityFeedback.modelValue = 'a'; await validityFeedback.updateComplete; expect(validityFeedback.$$slot('feedback').innerText).to.equal( 'This is error message for minLength', ); validityFeedback.modelValue = 'abc'; await validityFeedback.updateComplete; expect(validityFeedback.$$slot('feedback').innerText).to.equal( 'This is warning message for minLength', ); validityFeedback.modelValue = 'abcde'; await validityFeedback.updateComplete; expect(validityFeedback.$$slot('feedback').innerText).to.equal( 'This is info message for minLength', ); }); it('shows success message after fixing an error', async () => { // TODO: refactor to support integration via externalDependencies.element const validityFeedback = await fixture(html` <${defaultElementName} .errorValidators=${[[minLength, { min: 3 }]]} .successValidators=${[[alwaysFalse]]} >${lightDom} `); validityFeedback.modelValue = 'a'; await validityFeedback.updateComplete; expect(validityFeedback.$$slot('feedback').innerText).to.equal( 'This is error message for minLength', ); validityFeedback.modelValue = 'abcd'; await validityFeedback.updateComplete; expect(validityFeedback.$$slot('feedback').innerText).to.equal( 'This is success message for alwaysFalse', ); }); it(`shows only highest priority validation message determined by order of assignment of validators`, async () => { // TODO: refactor to support integration via externalDependencies.element const validityFeedback = await fixture(html` <${defaultElementName} .errorValidators=${[[containsCat], [minLength, { min: 4 }]]} >${lightDom} `); validityFeedback.modelValue = 'dog and dog'; await validityFeedback.updateComplete; expect(validityFeedback.$$slot('feedback').innerText).to.equal( 'This is error message for containsCat', ); validityFeedback.modelValue = 'dog'; await validityFeedback.updateComplete; expect(validityFeedback.$$slot('feedback').innerText).to.equal( 'This is error message for containsCat', ); validityFeedback.modelValue = 'cat'; await validityFeedback.updateComplete; expect(validityFeedback.$$slot('feedback').innerText).to.equal( 'This is error message for minLength', ); validityFeedback.modelValue = 'dog and cat'; await validityFeedback.updateComplete; expect(validityFeedback.$$slot('feedback').innerText).to.equal(''); }); it('supports randomized selection of multiple messages for the same validator', async () => { const randomTranslationsElement = defineCE( class extends ValidateMixin(LionLitElement) { static get properties() { return { ...super.properties, modelValue: { type: String, }, }; } translateMessage(rawKeys) { const translationData = { error: { containsLowercaseA: 'You should have a lowercase a', }, success: { randomAlwaysFalse: 'success.a, success.b, success.c, success.d', a: 'Good job!', b: 'You did great!', c: 'Looks good!', d: 'nice!', }, }; const keys = !Array.isArray(rawKeys) ? [rawKeys] : rawKeys; for (let i = 0; i < keys.length; i += 1) { const key = keys[i].split(':')[1]; const found = key.split('.').reduce((o, j) => o[j], translationData); if (found) { return found; } } return ''; } }, ); const mathRandom = Math.random; Math.random = () => 0; function randomAlwaysFalse() { return { randomAlwaysFalse: false }; } const randomTranslationsName = unsafeStatic(randomTranslationsElement); const randomTranslations = await fixture(html` <${randomTranslationsName} .modelValue=${'dog'} .errorValidators=${[[containsLowercaseA]]} .successValidators=${[[randomAlwaysFalse]]} > `); expect( randomTranslations.translateMessage('random-translations:error.containsLowercaseA'), ).to.equal('You should have a lowercase a'); expect(randomTranslations.translateMessage('random-translations:success.a')).to.equal( 'Good job!', ); expect(randomTranslations.$$slot('feedback').innerText).to.equal( 'You should have a lowercase a', ); randomTranslations.modelValue = 'cat'; await randomTranslations.updateComplete; expect(randomTranslations.$$slot('feedback').innerText).to.equal('Good job!'); Math.random = () => 0.25; randomTranslations.__lastGetSuccessResult = false; randomTranslations.modelValue = 'dog'; randomTranslations.modelValue = 'cat'; await randomTranslations.updateComplete; expect(randomTranslations.$$slot('feedback').innerText).to.equal('You did great!'); Math.random = mathRandom; // manually restore }); it('translates validity messages', async () => { localize.reset(); localize.addData('en-GB', 'lion-validate', { error: { minLength: 'You need to enter at least {validatorParams.min} characters.' }, }); localize.addData('de-DE', 'lion-validate', { error: { minLength: 'Es müssen mindestens {validatorParams.min} Zeichen eingegeben werden.', }, }); const validityFeedback = await fixture( html` <${defaultElementName} .modelValue=${'cat'} .errorValidators=${[[minLength, { min: 4 }]]} >${lightDom} `, () => ({ modelValue: 'cat', errorValidators: [[minLength, { min: 4 }]], }), ); expect(validityFeedback.$$slot('feedback').innerText).to.equal( 'You need to enter at least 4 characters.', ); localize.locale = 'de-DE'; await validityFeedback.updateComplete; expect(validityFeedback.$$slot('feedback').innerText).to.equal( 'Es müssen mindestens 4 Zeichen eingegeben werden.', ); }); describe('Field name', () => { beforeEach(() => { localizeTearDown(); localize.addData('en-GB', 'lion-validate', { error: { minLength: '{fieldName} needs more characters' }, }); }); it('allows to use field name in messages', async () => { const el = await fixture(html` <${tag} .label=${'myField'} .errorValidators=${[[minLength, { min: 4 }]]} .modelValue=${'cat'} >${lightDom} `); expect(el.$$slot('feedback').innerText).to.equal('myField needs more characters'); }); it('allows to configure field name for every validator message', async () => { const elNameStatic = { d: `${tagString}` }; const validityFeedback = await fixture(html` <${elNameStatic} .label="${'myField'}" .name="${'myName'}" .errorValidators=${[ [minLength, { min: 4, fieldName: 'overrideName' }], ]} .modelValue=${'cat'} >${lightDom} `); expect(validityFeedback.$$slot('feedback').innerText).to.equal( 'overrideName needs more characters', ); }); it('constructs field name from label or name (in this priority order)', async () => { const elNameStatic = { d: `${tagString}` }; // As seen in test above, configuring fieldName on validator level takes highest precedence const validityFeedback = await fixture(html` <${elNameStatic} .label="${'myField'}" .name="${'myName'}" .errorValidators=${[[minLength, { min: 4 }]]} .modelValue=${'cat'} >${lightDom} `); expect(validityFeedback.$$slot('feedback').innerText).to.equal( 'myField needs more characters', ); const validityFeedback2 = await fixture(html` <${elNameStatic} .name="${'myName'}" .errorValidators=${[[minLength, { min: 4 }]]} .modelValue=${'cat'} >${lightDom} `); expect(validityFeedback2.$$slot('feedback').innerText).to.equal( 'myName needs more characters', ); }); }); describe('Configuration meta data', () => { // In some cases, for instance in a group elememnt like fieldset, the validity state of the // children inputs need to be reflected on group level when asked for imperatively, although // the feedback needs to be displayed on input level and not on group level for these kind // of validators. it('allows for opting out of visibly rendering feedback via "hideFeedback"', async () => { const errorRenderer = defineCE( class extends HTMLElement { renderFeedback(validationStates, message) { const validator = message.list[0].data.validatorName; const hide = message.list[0].data.validatorConfig && message.list[0].data.validatorConfig.hideFeedback; if (validationStates.error && !hide) { this.innerText = `ERROR on ${validator}`; } else { this.innerText = ''; } } }, ); const errorRendererName = unsafeStatic(errorRenderer); // TODO: refactor to support integration via externalDependencies.element const element = await fixture(html` <${defaultElementName} .errorValidators=${[[containsLowercaseA], [alwaysFalse, {}, { hideFeedback: true }]]}> <${errorRendererName} slot="feedback"><${errorRendererName}> `); element.modelValue = 'dog'; await element.updateComplete; expect(element.$$slot('feedback').innerText).to.equal('ERROR on containsLowercaseA'); element.modelValue = 'cat'; await element.updateComplete; expect(element.$$slot('feedback').innerText).to.equal(''); }); }); /** * Order of keys should be like this * * ['lion-input-email', 'lion-validate'].forEach((namespace) => { * 1. ${namespace}+${validatorName}:${type}.${validatorName} * 2. ${namespace}:${type}.${validatorName} * }); * * Example: * * * 1. lion-input-email+isEmail:error:isEmail * 2. lion-input-email:error:isEmail * 3. lion-validate+isEmail:error.isEmail * 4. lion-validate:error.isEmail */ describe('Localize Priority', () => { it('adds a default namespace `lion-validate`', () => { expect(customElements.get(tagString).localizeNamespaces[0]).to.include.keys( 'lion-validate', ); }); it(`searches for the message in a specific order: 1. lion-validate+$validatorName:$type.$validatorName 2. lion-validate:$type.$validatorName `, async () => { // Tests are in 'reversed order', so we can increase prio by filling up localize storage const orderValidator = () => [() => ({ orderValidator: false })]; const el = await fixture(html` <${tag} .name=${'foo'} .errorValidators=${[orderValidator()]} .modelValue=${'10'}> ${lightDom} `); // reset the storage so that we can fill it in for each of 2 cases step by step localize.reset(); // 2. lion-validate localize.addData('en-GB', 'lion-validate', { error: { orderValidator: 'lion-validate : orderValidator', }, }); el._createMessageAndRenderFeedback(); expect(el.$$slot('feedback').innerText).to.equal('lion-validate : orderValidator'); // 1. lion-validate+orderValidator localize.addData('en-GB', 'lion-validate+orderValidator', { error: { orderValidator: 'lion-validate+orderValidator : orderValidator', }, }); el._createMessageAndRenderFeedback(); expect(el.$$slot('feedback').innerText).to.equal( 'lion-validate+orderValidator : orderValidator', ); }); it(`searches for the message in a specific order (when there is an extra namespace): 1. my-custom-namespace+$validatorName:$type.$validatorName 2. my-custom-namespace:$type.$validatorName 3. lion-validate+$validatorName:$type.$validatorName 4. lion-validate:$type.$validatorName `, async () => { // Tests are in 'reversed order', so we can increase prio by filling up localize storage const is12Validator = () => [modelValue => ({ is12Validator: modelValue === 12 })]; const orderName = defineCE( class extends ValidateMixin(LionLitElement) { static get properties() { return { ...super.properties, modelValue: { type: String } }; } static get localizeNamespaces() { return [ { 'my-custom-namespace': () => Promise.resolve({}) }, ...super.localizeNamespaces, ]; } }, ); const tagOrderName = unsafeStatic(orderName); const el = await fixture(html` <${tagOrderName} .name=${'bar'} .errorValidators=${[is12Validator()]} .modelValue=${'10'}> ${lightDom} `); // reset the storage so that we can fill it in for each of 4 cases step by step localize.reset(); // 4. lion-validate localize.addData('en-GB', 'lion-validate', { error: { is12Validator: 'lion-validate : is12Validator', }, }); el._createMessageAndRenderFeedback(); expect(el.$$slot('feedback').innerText).to.equal('lion-validate : is12Validator'); // 3. lion-validate+is12Validator localize.addData('en-GB', 'lion-validate+is12Validator', { error: { is12Validator: 'lion-validate+is12Validator : is12Validator', }, }); el._createMessageAndRenderFeedback(); expect(el.$$slot('feedback').innerText).to.equal( 'lion-validate+is12Validator : is12Validator', ); // 2. my-custom-namespace localize.addData('en-GB', 'my-custom-namespace', { error: { is12Validator: 'my-custom-namespace : is12Validator', }, }); el._createMessageAndRenderFeedback(); expect(el.$$slot('feedback').innerText).to.equal('my-custom-namespace : is12Validator'); // 1. my-custom-namespace+is12Validator localize.addData('en-GB', 'my-custom-namespace+is12Validator', { error: { is12Validator: 'my-custom-namespace+is12Validator : is12Validator', }, }); el._createMessageAndRenderFeedback(); expect(el.$$slot('feedback').innerText).to.equal( 'my-custom-namespace+is12Validator : is12Validator', ); }); }); }); describe(`Asynchronous validation [to-be-implemented] ${suffixName}`, () => { it('handles promises as custom validator functions [to-be-implemented]', async () => {}); it('sets a class "state-pending" when validation is in progress [to-be-implemented]', async () => {}); it('debounces async validation for performance [to-be-implemented]', async () => {}); it('cancels and reschedules async validation on value change [to-be-implemented]', async () => {}); it('blocks input when option "block-on-pending" is set [to-be-implemented]', async () => { // This might also be styles on state-pending => disable pointer-events and style as blocked }); it('lets developer configure condition for asynchronous validation [to-be-implemented]', async () => { // This can be blur when input needs to be blocked. // Can be implemented as validateAsyncCondition(), returning boolean // Will first look at , then at .form }); }); });