diff --git a/packages/survey-core/src/validator.ts b/packages/survey-core/src/validator.ts index 2eae4e62a0..348d030cdd 100644 --- a/packages/survey-core/src/validator.ts +++ b/packages/survey-core/src/validator.ts @@ -481,23 +481,22 @@ export class ExpressionValidator extends SurveyValidator { public getType(): string { return "expressionvalidator"; } - public get isValidateAllValues() { + public get isValidateAllValues(): boolean { return true; } public get isAsync(): boolean { - if (!this.ensureConditionRunner()) return false; + if (!this.ensureConditionRunner(false)) return false; return this.conditionRunner.isAsync; } public get isRunning(): boolean { return this.isRunningValue; } - public validate( - value: any, - name: string = null, - values: any = null, - properties: any = null - ): ValidatorResult { - if (!this.ensureConditionRunner()) return null; + public validate(value: any, name: string = null, values: any = null, properties: any = null): ValidatorResult { + if (!this.expression) return null; + if(!!this.conditionRunner) { + this.conditionRunner.onRunComplete = null; + } + this.ensureConditionRunner(true); this.conditionRunner.onRunComplete = (res) => { this.isRunningValue = false; if (!!this.onAsyncCompleted) { @@ -510,22 +509,22 @@ export class ExpressionValidator extends SurveyValidator { this.isRunningValue = false; return this.generateError(res, value, name); } - protected generateError(res: boolean, value: any, name: string) { + protected generateError(res: boolean, value: any, name: string): ValidatorResult { if (!res) { return new ValidatorResult(value, this.createCustomError(name)); } return null; } - protected getDefaultErrorText(name: string) { + protected getDefaultErrorText(name: string): string { return this.getLocalizationFormatString("invalidExpression", this.expression); } - protected ensureConditionRunner(): boolean { - if (!!this.conditionRunner) { + private ensureConditionRunner(reNew: boolean): boolean { + if (!this.expression) return false; + if(reNew || !this.conditionRunner) { + this.conditionRunner = new ConditionRunner(this.expression); + } else { this.conditionRunner.expression = this.expression; - return true; } - if (!this.expression) return false; - this.conditionRunner = new ConditionRunner(this.expression); return true; } /** diff --git a/packages/survey-core/tests/surveyvalidatortests.ts b/packages/survey-core/tests/surveyvalidatortests.ts index 66ea76c6f7..6af743cdc9 100644 --- a/packages/survey-core/tests/surveyvalidatortests.ts +++ b/packages/survey-core/tests/surveyvalidatortests.ts @@ -554,3 +554,49 @@ QUnit.test("settings.readOnly.enableValidation option", function(assert) { assert.equal(q1.errors.length, 1, "There is an error"); settings.readOnly.enableValidation = false; }); +QUnit.test("Async expression validators creates several errors", function(assert) { + const asynList = new Array(); + function asyncFunc(params) { + asynList.push(this.returnResult); + } + FunctionFactory.Instance.register("asyncFunc", asyncFunc, true); + function callAsyncList(): void { + while(asynList.length > 0) { + let i = asynList.length % 2; + if(i >= asynList.length) i = 0; + asynList[i](false); + asynList.splice(i, 1); + } + } + const survey = new SurveyModel({ + elements: [ + { + type: "text", + name: "q1", + validators: [{ type: "expression", expression: "asyncFunc({question1})" }] + }, + { + type: "text", + name: "q2", + validators: [{ type: "expression", expression: "asyncFunc({question1})" }] + } + ] + }); + const q1 = survey.getQuestionByName("q1"); + const q2 = survey.getQuestionByName("q2"); + q1.value = "a"; + q1.valule = "b"; + survey.validate(true); + assert.equal(asynList.length, 2, "#1.0"); + callAsyncList(); + assert.equal(q1.errors.length, 1, "#1.1"); + assert.equal(q2.errors.length, 1, "#1.2"); + q1.value = "aa"; + callAsyncList(); + assert.equal(q1.errors.length, 1, "#2.1"); + assert.equal(q2.errors.length, 1, "#2.2"); + q2.value = "bb"; + callAsyncList(); + assert.equal(q1.errors.length, 1, "#3.1"); + assert.equal(q2.errors.length, 1, "#3.2"); +});