feat(form-core): allow enums as outcome of a Validator

This commit is contained in:
Thijs Louisse 2022-03-16 10:03:34 +01:00
parent e457ce73bb
commit 7016a150dc
5 changed files with 341 additions and 183 deletions

View file

@ -0,0 +1,5 @@
---
'@lion/form-core': minor
---
Validation: allow enums as outcome of a Validator

View file

@ -17,6 +17,10 @@ import { FormControlMixin } from '../FormControlMixin.js';
/**
* @typedef {import('../../types/validate/ValidateMixinTypes').ValidateMixin} ValidateMixin
* @typedef {import('../../types/validate/ValidateMixinTypes').ValidationType} ValidationType
* @typedef {import('../../types/validate/ValidateMixinTypes').ValidateHost} ValidateHost
* @typedef {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} ValidateHostConstructor
* @typedef {{validator:Validator; outcome:boolean|string}} ValidationResultEntry
* @typedef {{[type:string]: {[validatorName:string]:boolean|string}}} ValidationStates
*/
/**
@ -159,7 +163,7 @@ export const ValidateMixinImplementation = superclass =>
*/
this.showsFeedbackFor = [];
// TODO: [v1] make this fully private (preifix __)?
// TODO: [v1] make this fully private (prefix __)?
/**
* A temporary storage to transition from hasFeedbackFor to showsFeedbackFor
* @type {ValidationType[]}
@ -171,7 +175,7 @@ export const ValidateMixinImplementation = superclass =>
/**
* The outcome of a validation 'round'. Keyed by ValidationType and Validator name
* @readOnly
* @type {Object.<string, Object.<string, boolean>>}
* @type {ValidationStates}
*/
this.validationStates = {};
@ -206,44 +210,45 @@ export const ValidateMixinImplementation = superclass =>
/**
* The amount of feedback messages that will visible in LionValidationFeedback
* @configurable
* @protected
*/
this._visibleMessagesAmount = 1;
/**
* @type {Validator[]}
* @type {ValidationResultEntry[]}
* @private
*/
this.__syncValidationResult = [];
/**
* @type {Validator[]}
* @type {ValidationResultEntry[]}
* @private
*/
this.__asyncValidationResult = [];
/**
* Aggregated result from sync Validators, async Validators and ResultValidators
* @type {Validator[]}
* @type {ValidationResultEntry[]}
* @private
*/
this.__validationResult = [];
/**
* @type {Validator[]}
* @type {ValidationResultEntry[]}
* @private
*/
this.__prevValidationResult = [];
/**
* @type {Validator[]}
* @type {ValidationResultEntry[]}
* @private
*/
this.__prevShownValidationResult = [];
/**
* The updated children validity affects the validity of the parent. Helper to recompute
* validatity of parent FormGroup
* validity of parent FormGroup
* @private
*/
this.__childModelValueChanged = false;
@ -384,6 +389,14 @@ export const ValidateMixinImplementation = superclass =>
* @desc step a1-3 + b (as explained in `validate()`)
*/
async __executeValidators() {
/**
* Allows Application Developer to wait for (async) validation
* @example
* ```js
* await el.validateComplete;
* ```
* @type {Promise<boolean>}
*/
this.validateComplete = new Promise(resolve => {
this.__validateCompleteResolve = resolve;
});
@ -410,7 +423,7 @@ export const ValidateMixinImplementation = superclass =>
const isEmpty = this.__isEmpty(value);
if (isEmpty) {
if (requiredValidator) {
this.__syncValidationResult = [requiredValidator];
this.__syncValidationResult = [{ validator: requiredValidator, outcome: true }];
}
this.__finishValidation({ source: 'sync' });
return;
@ -451,9 +464,12 @@ export const ValidateMixinImplementation = superclass =>
*/
__executeSyncValidators(syncValidators, value, { hasAsync }) {
if (syncValidators.length) {
this.__syncValidationResult = syncValidators.filter(v =>
v.execute(value, v.param, { node: this }),
);
this.__syncValidationResult = syncValidators
.map(v => ({
validator: v,
outcome: /** @type {boolean|string} */ (v.execute(value, v.param, { node: this })),
}))
.filter(v => Boolean(v.outcome));
}
this.__finishValidation({ source: 'sync', hasAsync });
}
@ -468,10 +484,15 @@ export const ValidateMixinImplementation = superclass =>
if (asyncValidators.length) {
this.isPending = true;
const resultPromises = asyncValidators.map(v => v.execute(value, v.param, { node: this }));
const booleanResults = await Promise.all(resultPromises);
this.__asyncValidationResult = booleanResults
.map((r, i) => asyncValidators[i]) // Create an array of Validators
.filter((v, i) => booleanResults[i]); // Only leave the ones returning true
const asyncExecutionResults = await Promise.all(resultPromises);
this.__asyncValidationResult = asyncExecutionResults
.map((r, i) => ({
validator: asyncValidators[i],
outcome: /** @type {boolean|string} */ (asyncExecutionResults[i]),
}))
.filter(v => Boolean(v.outcome));
this.__finishValidation({ source: 'async' });
this.isPending = false;
}
@ -479,7 +500,7 @@ export const ValidateMixinImplementation = superclass =>
/**
* step b (as explained in `validate()`), called by __finishValidation
* @param {Validator[]} regularValidationResult result of steps 1-3
* @param {{validator:Validator; outcome: boolean|string;}[]} regularValidationResult result of steps 1-3
* @private
*/
__executeResultValidators(regularValidationResult) {
@ -490,13 +511,21 @@ export const ValidateMixinImplementation = superclass =>
})
);
return resultValidators.filter(v =>
// Map everything to Validator[] for backwards compatibility
return resultValidators
.map(v => ({
validator: v,
outcome: /** @type {boolean|string} */ (
v.executeOnResults({
regularValidationResult,
prevValidationResult: this.__prevValidationResult,
prevShownValidationResult: this.__prevShownValidationResult,
}),
);
regularValidationResult: regularValidationResult.map(entry => entry.validator),
prevValidationResult: this.__prevValidationResult.map(entry => entry.validator),
prevShownValidationResult: this.__prevShownValidationResult.map(
entry => entry.validator,
),
})
),
}))
.filter(v => Boolean(v.outcome));
}
/**
@ -512,35 +541,32 @@ export const ValidateMixinImplementation = superclass =>
const resultOutCome = this.__executeResultValidators(syncAndAsyncOutcome);
this.__validationResult = [...resultOutCome, ...syncAndAsyncOutcome];
// this._storeResultsOnInstance(this.__validationResult);
const ctor =
/** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (
this.constructor
);
const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
/** @type {Object.<string, Object.<string, boolean>>} */
/** @type {ValidationStates} */
const validationStates = ctor.validationTypes.reduce(
(acc, type) => ({ ...acc, [type]: {} }),
{},
);
this.__validationResult.forEach(v => {
if (!validationStates[v.type]) {
validationStates[v.type] = {};
this.__validationResult.forEach(({ validator, outcome }) => {
if (!validationStates[validator.type]) {
validationStates[validator.type] = {};
}
const vCtor = /** @type {typeof Validator} */ (v.constructor);
validationStates[v.type][vCtor.validatorName] = true;
const vCtor = /** @type {typeof Validator} */ (validator.constructor);
validationStates[validator.type][vCtor.validatorName] = outcome;
});
this.validationStates = validationStates;
this.hasFeedbackFor = [...new Set(this.__validationResult.map(v => v.type))];
this.hasFeedbackFor = [
...new Set(this.__validationResult.map(({ validator }) => validator.type)),
];
/** private event that should be listened to by LionFieldSet */
this.dispatchEvent(new Event('validate-performed', { bubbles: true }));
if (source === 'async' || !hasAsync) {
if (this.__validateCompleteResolve) {
// @ts-ignore [allow-private]
this.__validateCompleteResolve();
this.__validateCompleteResolve(true);
}
}
}
@ -587,10 +613,7 @@ export const ValidateMixinImplementation = superclass =>
console.error(errorMessage, this);
throw new Error(errorMessage);
}
const ctor =
/** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (
this.constructor
);
const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
if (ctor.validationTypes.indexOf(v.type) === -1) {
const vCtor = /** @type {typeof Validator} */ (v.constructor);
// throws in constructor are not visible to end user so we do both
@ -640,14 +663,14 @@ export const ValidateMixinImplementation = superclass =>
*/
/**
* @param {Validator[]} validators list of objects having a .getMessage method
* @param {ValidationResultEntry[]} validationResults list of objects having a .getMessage method
* @return {Promise.<FeedbackMessage[]>}
* @private
*/
async __getFeedbackMessages(validators) {
async __getFeedbackMessages(validationResults) {
let fieldName = await this.fieldName;
return Promise.all(
validators.map(async validator => {
validationResults.map(async ({ validator, outcome }) => {
if (validator.config.fieldName) {
fieldName = await validator.config.fieldName;
}
@ -656,6 +679,7 @@ export const ValidateMixinImplementation = superclass =>
modelValue: this.modelValue,
formControl: this,
fieldName,
outcome,
});
return { message, type: validator.type, validator };
}),
@ -690,10 +714,19 @@ export const ValidateMixinImplementation = superclass =>
if (this.showsFeedbackFor.length > 0) {
this.__feedbackQueue.add(async () => {
/** @type {Validator[]} */
this.__prioritizedResult = this._prioritizeAndFilterFeedback({
validationResult: this.__validationResult,
const prioritizedValidators = this._prioritizeAndFilterFeedback({
validationResult: this.__validationResult.map(entry => entry.validator),
});
this.__prioritizedResult = prioritizedValidators
.map(v => {
const found = /** @type {ValidationResultEntry} */ (
this.__validationResult.find(r => v === r.validator)
);
return found;
})
.filter(Boolean);
if (this.__prioritizedResult.length > 0) {
this.__prevShownValidationResult = this.__prioritizedResult;
}
@ -732,12 +765,12 @@ export const ValidateMixinImplementation = superclass =>
}
/**
* Allows the end user to specify when a feedback message should be shown
* Allows the Application Developer to specify when a feedback message should be shown
* @example
* ```js
* feedbackCondition(type, meta, defaultCondition) {
* if (type === 'info') {
* return return;
* return true;
* } else if (type === 'prefilledOnly') {
* return meta.prefilled;
* }
@ -775,7 +808,9 @@ export const ValidateMixinImplementation = superclass =>
);
}
/** @param {import('@lion/core').PropertyValues} changedProperties */
/**
* @param {import('@lion/core').PropertyValues} changedProperties
*/
updated(changedProperties) {
super.updated(changedProperties);
@ -783,10 +818,7 @@ export const ValidateMixinImplementation = superclass =>
changedProperties.has('shouldShowFeedbackFor') ||
changedProperties.has('hasFeedbackFor')
) {
const ctor =
/** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (
this.constructor
);
const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
// Necessary typecast because types aren't smart enough to understand that we filter out undefined
this.showsFeedbackFor = /** @type {string[]} */ (
ctor.validationTypes
@ -822,10 +854,7 @@ export const ValidateMixinImplementation = superclass =>
* @protected
*/
_updateShouldShowFeedbackFor() {
const ctor =
/** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (
this.constructor
);
const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
// Necessary typecast because types aren't smart enough to understand that we filter out undefined
const newShouldShowFeedbackFor = /** @type {string[]} */ (
@ -848,18 +877,15 @@ export const ValidateMixinImplementation = superclass =>
}
/**
* Orders all active validators in this.__validationResult. Can
* also filter out occurrences (based on interaction states)
* Orders all active validators in this.__validationResult.
* Can also filter out occurrences (based on interaction states)
* @overridable
* @param {{ validationResult: Validator[] }} opts
* @return {Validator[]} ordered list of Validators with feedback messages visible to the end user
* @protected
*/
_prioritizeAndFilterFeedback({ validationResult }) {
const ctor =
/** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (
this.constructor
);
const ctor = /** @type {ValidateHostConstructor} */ (this.constructor);
const types = ctor.validationTypes;
// Sort all validators based on the type provided.
const res = validationResult

View file

@ -1,44 +1,65 @@
/**
* @typedef {object} MessageData
* @property {*} [MessageData.modelValue]
* @property {string} [MessageData.fieldName]
* @property {HTMLElement} [MessageData.formControl]
* @property {string} [MessageData.type]
* @property {Object.<string,?>} [MessageData.config]
* @property {string} [MessageData.name]
* @typedef {import('./types').FeedbackMessageData} FeedbackMessageData
* @typedef {import('./types').ValidatorParam} ValidatorParam
* @typedef {import('./types').ValidatorConfig} ValidatorConfig
* @typedef {import('./types').ValidatorOutcome} ValidatorOutcome
* @typedef {import('./types').ValidatorName} ValidatorName
* @typedef {import('./types').ValidationType} ValidationType
* @typedef {import('../FormControlMixin').FormControlHost} FormControlHost
*/
export class Validator {
// TODO: support attribute validators like <my-el my-validator=${dynamicParam}></my-el> =>
// register in a ValidateService that is read by Validator and adds these attrs in properties
// object.
// They would become like configurable
// [global attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes)
// for FormControls.
export class Validator extends EventTarget {
/**
*
* @param {?} [param]
* @param {Object.<string,?>} [config]
* @param {ValidatorParam} [param]
* @param {ValidatorConfig} [config]
*/
constructor(param, config) {
this.__fakeExtendsEventTarget();
super();
/** @type {?} */
/** @type {ValidatorParam} */
this.__param = param;
/** @type {Object.<string,?>} */
/** @type {ValidatorConfig} */
this.__config = config || {};
this.type = (config && config.type) || 'error'; // Default type supported by ValidateMixin
}
static get validatorName() {
return '';
}
static get async() {
return false;
/** @type {ValidationType} */
this.type = config?.type || 'error'; // Default type supported by ValidateMixin
}
/**
* @desc The function that returns a Boolean
* @param {?} [modelValue]
* @param {?} [param]
* @param {{}} [config]
* @returns {Boolean|Promise<Boolean>}
* The name under which validation results get registered. For convience and predictability, this
* should always be the same as the constructor name (since it will be obfuscated in js builds,
* we need to provide it separately).
* @type {ValidatorName}
*/
static validatorName = '';
/**
* Whether the validator is asynchronous or not. When true., this means execute function returns
* a Promise. This can be handy for:
* - server side calls
* - validations that are dependent on lazy loaded resources (they can be async until the dependency
* is loaded)
* @type {boolean}
*/
static async = false;
/**
* The function that returns a validity outcome. When we need to shpw feedback,
* it should return true, otherwise false. So when an error\info|warning|success message
* needs to be shown, return true. For async Validators, the function canretun a Promise.
* It's also possible to return an enum. Let's say that a phone number can have multiple
* states: 'invalid-country-code' | 'too-long' | 'too-short'
* Those states can be retrieved in the getMessage
* @param {any} modelValue
* @param {ValidatorParam} [param]
* @param {ValidatorConfig} [config]
* @returns {ValidatorOutcome|Promise<ValidatorOutcome>}
*/
// eslint-disable-next-line no-unused-vars, class-methods-use-this
execute(modelValue, param, config) {
@ -51,32 +72,85 @@ export class Validator {
return true;
}
/**
* The first argument of the constructor, for instance 3 in `new MinLength(3)`. Will
* be stored on Validator instance and passed to `execute` function
* @example
* ```js
* // Store reference to Validator instance
* const myValidatorInstance = new MyValidator(1);
* // Use this instance initially on a FormControl (that uses ValidateMixin)
* render(html`<validatable-element .validators="${[myValidatorInstance]}"></validatable-element>`, document.body);
* // Based on some event, we need to change the param
* myValidatorInstance.param = 2;
* ```
* @property {ValidatorParam}
*/
set param(p) {
this.__param = p;
if (this.dispatchEvent) {
/**
* This event is listened for by ValidateMixin. Whenever the validation parameter has
* changed, the FormControl will revalidate itself
*/
this.dispatchEvent(new Event('param-changed'));
}
}
get param() {
return this.__param;
}
/**
* The second argument of the constructor, for instance
* `new MinLength(3, {getFeedMessage: async () => 'too long'})`.
* Will be stored on Validator instance and passed to `execute` function.
* @example
* ```js
* // Store reference to Validator instance
* const myValidatorInstance = new MyValidator(1, {getMessage() => 'x'});
* // Use this instance initially on a FormControl (that uses ValidateMixin)
* render(html`<validatable-element .validators="${[myValidatorInstance]}"></validatable-element>`, document.body);
* // Based on some event, we need to change the param
* myValidatorInstance.config = {getMessage() => 'y'};
* ```
* @property {ValidatorConfig}
*/
set config(c) {
this.__config = c;
if (this.dispatchEvent) {
/**
* This event is listened for by ValidateMixin. Whenever the validation config has
* changed, the FormControl will revalidate itself
*/
this.dispatchEvent(new Event('config-changed'));
}
}
get config() {
return this.__config;
}
/**
* @overridable
* @param {MessageData} [data]
* @returns {Promise<string|Node>}
* This is a protected method that usually should not be overridden. It is called by ValidateMixin
* and it gathers data to be passed to getMessage functions found:
* - `this.config.getMessage`, locally provided by consumers of the Validator (overrides global getMessage)
* - `MyValidator.getMessage`, globally provided by creators or consumers of the Validator
*
* Confusion can arise because of similarities with former mentioned methods. In that regard, a
* better name for this function would have been _pepareDataAndCallHighestPrioGetMessage.
* @example
* ```js
* class MyValidator extends Validator {
* // ...
* // 1. globally defined
* static async getMessage() {
* return 'lowest prio, defined globally by Validator author'
* }
* }
* // 2. globally overridden
* MyValidator.getMessage = async() => 'overrides already configured message';
* // 3. locally overridden
* new MyValidator(myParam, { getMessage: async() => 'locally defined, always wins' });
* ```
* @param {Partial<FeedbackMessageData>} [data]
* @returns {Promise<string|Element>}
* @protected
*/
async _getMessage(data) {
@ -101,9 +175,20 @@ export class Validator {
}
/**
* Called inside Validator.prototype._getMessage (see explanation).
* @example
* ```js
* class MyValidator extends Validator {
* static async getMessage() {
* return 'lowest prio, defined globally by Validator author'
* }
* }
* // globally overridden
* MyValidator.getMessage = async() => 'overrides already configured message';
* ```
* @overridable
* @param {MessageData} [data]
* @returns {Promise<string|Node>}
* @param {Partial<FeedbackMessageData>} [data]
* @returns {Promise<string|Element>}
*/
// eslint-disable-next-line no-unused-vars
static async getMessage(data) {
@ -111,12 +196,38 @@ export class Validator {
}
/**
* @param {HTMLElement} formControl
* Validators are allowed to have knowledge about FormControls.
* In some cases (in case of the Required Validator) we wanted to enhance accessibility by
* adding [aria-required]. Also, it would be possible to write an advanced MinLength
* Validator that adds a .preprocessor that restricts from typing too many characters
* (like the native [minlength] validator).
* Will be called when Validator is added to FormControl.validators.
* @example
* ```js
* onFormControlConnect(formControl) {
* if(formControl.inputNode) {
* inputNode.setAttribute('aria-required', 'true');
* }
* }
*
* ```
* @configurable
* @param {FormControlHost} formControl
*/
onFormControlConnect(formControl) {} // eslint-disable-line
/**
* @param {HTMLElement} formControl
* Also see `onFormControlConnect`.
* Will be called when Validator is removed from FormControl.validators.
* @example
* ```js
* onFormControlDisconnect(formControl) {
* if(formControl.inputNode) {
* inputNode.removeAttribute('aria-required');
* }
* }
* @configurable
* @param {FormControlHost} formControl
*/
onFormControlDisconnect(formControl) {} // eslint-disable-line
@ -130,41 +241,6 @@ export class Validator {
* - Or, when a webworker was started, its process could be aborted and then restarted.
*/
abortExecution() {} // eslint-disable-line
/**
* @private
*/
__fakeExtendsEventTarget() {
const delegate = document.createDocumentFragment();
/**
*
* @param {string} type
* @param {EventListener} listener
* @param {Object} [opts]
*/
const delegatedAddEventListener = (type, listener, opts) =>
delegate.addEventListener(type, listener, opts);
/**
* @param {string} type
* @param {EventListener} listener
* @param {Object} [opts]
*/
const delegatedRemoveEventListener = (type, listener, opts) =>
delegate.removeEventListener(type, listener, opts);
/**
* @param {Event|CustomEvent} event
*/
const delegatedDispatchEvent = event => delegate.dispatchEvent(event);
this.addEventListener = delegatedAddEventListener;
this.removeEventListener = delegatedRemoveEventListener;
this.dispatchEvent = delegatedDispatchEvent;
}
}
// For simplicity, a default validator only handles one state:

View file

@ -70,7 +70,7 @@ export function runValidateMixinSuite(customConfig) {
*/
describe('Validation initiation', () => {
it('throws and console.errors if adding not Validator instances to the validators array', async () => {
it('throws and console.errors if adding non Validator instances to the validators array', async () => {
// we throw and console error as constructor throw are not visible to the end user
const stub = sinon.stub(console, 'error');
const el = /** @type {ValidateElement} */ (await fixture(html`<${tag}></${tag}>`));
@ -93,7 +93,7 @@ export function runValidateMixinSuite(customConfig) {
stub.restore();
});
it('throws and console error if adding a not supported Validator type', async () => {
it('throws a console error if adding a non supported Validator type', async () => {
// we throw and console error to improve DX
const stub = sinon.stub(console, 'error');
const errorMessage = `This component does not support the validator type "major error" used in "MajorValidator". You may change your validators type or add it to the components "static get validationTypes() {}".`;
@ -720,7 +720,10 @@ export function runValidateMixinSuite(customConfig) {
// @ts-ignore [allow-private] in test
const totalValidationResult = el.__validationResult;
expect(totalValidationResult).to.eql([resultV, validator]);
expect(totalValidationResult).to.eql([
{ validator: resultV, outcome: true },
{ validator, outcome: true },
]);
});
});
@ -1049,7 +1052,7 @@ export function runValidateMixinSuite(customConfig) {
await fixture(html`
<${tag}
.modelValue=${'123'}
.validators=${[new MinLength(3, { message: 'foo' })]}>
.validators=${[new MinLength(3, { getMessage: async () => 'foo' })]}>
<input slot="input">
</${tag}>`)
);
@ -1352,14 +1355,14 @@ export function runValidateMixinSuite(customConfig) {
};
}
// @ts-ignore
/**
* @param {string} type
*/
_showFeedbackConditionFor(type) {
switch (type) {
case 'error':
// @ts-ignore
return ['A', 'B'].includes(this.modelValue);
default:
// @ts-ignore
return ['B', 'C'].includes(this.modelValue);
}
}
@ -1384,12 +1387,10 @@ export function runValidateMixinSuite(customConfig) {
['D', []],
]) {
el.modelValue = modelValue;
// eslint-disable-next-line no-await-in-loop
await el.updateComplete;
// eslint-disable-next-line no-await-in-loop
await el.feedbackComplete;
// @ts-ignore
// @ts-expect-error [allow-protected]
const resultOrder = el._feedbackNode.feedbackData.map(v => v.type);
expect(resultOrder).to.deep.equal(expected);
}

View file

@ -7,6 +7,10 @@ import sinon from 'sinon';
import { DefaultSuccess, MinLength, Required, ValidateMixin, Validator } from '../index.js';
import { AlwaysInvalid } from '../test-helpers/index.js';
/**
* @typedef {import('../src/validate/types').FeedbackMessageData} FeedbackMessageData
*/
export function runValidateMixinFeedbackPart() {
describe('Validity Feedback', () => {
beforeEach(() => {
@ -303,7 +307,7 @@ export function runValidateMixinFeedbackPart() {
await fixture(html`
<${tag}
.submitted=${true}
.validators=${[new MinLength(3, { getMessage: () => 'custom via config' })]}
.validators=${[new MinLength(3, { getMessage: async () => 'custom via config' })]}
>${lightDom}</${tag}>
`)
);
@ -397,7 +401,7 @@ export function runValidateMixinFeedbackPart() {
});
});
describe('Meta data', () => {
describe('FeedbackMessageData', () => {
it('".getMessage()" gets a reference to formControl, params, modelValue and type', async () => {
class ValidateElementCustomTypes extends ValidateMixin(LitElement) {
static get validationTypes() {
@ -431,6 +435,7 @@ export function runValidateMixinFeedbackPart() {
fieldName: '',
type: 'x',
name: 'MinLength',
outcome: true,
});
const instanceMessageSpy = sinon.spy();
@ -457,6 +462,7 @@ export function runValidateMixinFeedbackPart() {
fieldName: '',
type: 'error',
name: 'MinLength',
outcome: true,
});
});
@ -485,7 +491,7 @@ export function runValidateMixinFeedbackPart() {
fieldName: 'myField',
type: 'error',
name: 'MinLength',
});
outcome: true,
});
});
@ -520,6 +526,50 @@ export function runValidateMixinFeedbackPart() {
fieldName: 'myFieldViaCfg',
type: 'error',
name: 'MinLength',
outcome: true,
});
});
it('".getMessage()" gets .outcome, which can be "true" or an enum', async () => {
class EnumOutComeValidator extends Validator {
static validatorName = 'EnumOutCome';
execute() {
return 'a-string-instead-of-bool';
}
/**
* @param {FeedbackMessageData} meta
* @returns
*/
static async getMessage({ outcome }) {
const results = {
'a-string-instead-of-bool': 'Msg based on enum output',
};
return results[/** @type {string} */ (outcome)];
}
}
const enumOutComeValidator = new EnumOutComeValidator();
const spy = sinon.spy(
/** @type {typeof EnumOutComeValidator} */ (enumOutComeValidator.constructor),
'getMessage',
);
const el = /** @type {ValidateElement} */ (
await fixture(html`
<${tag}
.submitted=${true}
.validators=${[enumOutComeValidator]}
.modelValue=${'cat'}
>${lightDom}</${tag}>
`)
);
await el.updateComplete;
await el.feedbackComplete;
const getMessageArs = spy.args[0][0];
expect(getMessageArs.outcome).to.equal('a-string-instead-of-bool');
});
});