diff --git a/eclipse-scout-core/src/form/Form.ts b/eclipse-scout-core/src/form/Form.ts index 0e345ecbc7e..6d080eca487 100644 --- a/eclipse-scout-core/src/form/Form.ts +++ b/eclipse-scout-core/src/form/Form.ts @@ -454,11 +454,18 @@ export class Form extends Widget implements FormModel, DisplayParent { // If form has been closed right after it was opened ignore the load result return; } + this.setData(data); this.importData(); - this._setFormLoading(false); - this.formLoaded = true; - this.trigger('load'); + + // Wait for async field validators to complete so the value is set when the form loading finishes + let pendingFields = FormLifecycle.validateFormFields(this).pendingElements; + if (pendingFields.length) { + return $.promiseAll(pendingFields.map(field => field.promise)) + .then(() => this._loadingDone()); + } + + this._loadingDone(); }) .always(() => this._setFormLoading(false)); } catch (error) { @@ -467,6 +474,12 @@ export class Form extends Widget implements FormModel, DisplayParent { } } + protected _loadingDone() { + this._setFormLoading(false); + this.formLoaded = true; + this.trigger('load'); + } + protected _setFormLoading(loading: boolean) { this._setProperty('formLoading', loading); } diff --git a/eclipse-scout-core/src/form/fields/BasicField.ts b/eclipse-scout-core/src/form/fields/BasicField.ts index 3e431bf19c6..3e57e05ed25 100644 --- a/eclipse-scout-core/src/form/fields/BasicField.ts +++ b/eclipse-scout-core/src/form/fields/BasicField.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -88,13 +88,13 @@ export abstract class BasicField | void { if (this._displayTextModifiedTimeoutId !== null) { // Cancel pending "acceptInput(true)" call (see _onDisplayTextModified) and execute it now clearTimeout(this._displayTextModifiedTimeoutId); this._displayTextModifiedTimeoutId = null; } - super.acceptInput(whileTyping); + return super.acceptInput(whileTyping); } protected override _renderDisplayText() { diff --git a/eclipse-scout-core/src/form/fields/FormField.ts b/eclipse-scout-core/src/form/fields/FormField.ts index 1fbd10929e8..dcb9ceb10d6 100644 --- a/eclipse-scout-core/src/form/fields/FormField.ts +++ b/eclipse-scout-core/src/form/fields/FormField.ts @@ -368,7 +368,7 @@ export class FormField extends Widget implements FormFieldModel { } /** - * Adds the given (functional) error status to the list of error status. Prefer this function over #setErrorStatus + * Adds the given (functional) error status to the list of error status. Prefer this function over {@link setErrorStatus} * when you don't want to mess with the internal error states of the field (parsing, validation). */ addErrorStatus(errorStatus: string | Status) { @@ -1643,6 +1643,7 @@ export type ValidationResult = { errorStatus?: Status; field: FormField; label: string; + promise?: JQuery.Promise; reveal: () => void; visitResult?: TreeVisitResult; }; diff --git a/eclipse-scout-core/src/form/fields/FormFieldValidationResultProvider.ts b/eclipse-scout-core/src/form/fields/FormFieldValidationResultProvider.ts index 5bd88a4e294..22ce4ec16f0 100644 --- a/eclipse-scout-core/src/form/fields/FormFieldValidationResultProvider.ts +++ b/eclipse-scout-core/src/form/fields/FormFieldValidationResultProvider.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2024 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -7,7 +7,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -import {fields, FormField, InitModelOf, ObjectModel, ObjectWithType, scout, SomeRequired, Status, ValidationResult} from '../..'; +import {fields, FormField, InitModelOf, ObjectModel, ObjectWithType, scout, SomeRequired, Status, ValidationResult, ValueField} from '../..'; export class FormFieldValidationResultProvider implements ObjectWithType { declare model: FormFieldValidationResultProviderModel; @@ -23,19 +23,28 @@ export class FormFieldValidationResultProvider implements ObjectWithType { provide(errorStatus: Status): ValidationResult { const validByErrorStatus = !errorStatus || errorStatus.isValid(); const validByMandatory = !this.field.mandatory || !this.field.empty; - const valid = validByErrorStatus && validByMandatory; + const validatePendingPromise = this._validatePendingPromise(); + const valid = validByErrorStatus && validByMandatory && !validatePendingPromise; return { valid, validByMandatory, errorStatus, field: this.field, label: this.field.label, + promise: validatePendingPromise, reveal: () => { fields.selectAllParentTabsOf(this.field); this.field.focus(); } }; } + + protected _validatePendingPromise(): JQuery.Promise { + if (this.field instanceof ValueField && this.field.validatePending) { + return this.field.when('propertyChange:validatePending').then(() => undefined); + } + return null; + } } export interface FormFieldValidationResultProviderModel extends ObjectModel { diff --git a/eclipse-scout-core/src/form/fields/ValueField.ts b/eclipse-scout-core/src/form/fields/ValueField.ts index 17d60365b82..fe1cbd1025b 100644 --- a/eclipse-scout-core/src/form/fields/ValueField.ts +++ b/eclipse-scout-core/src/form/fields/ValueField.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -8,7 +8,7 @@ * SPDX-License-Identifier: EPL-2.0 */ import { - AbstractLayout, aria, arrays, EnumObject, focusUtils, FormField, InitModelOf, objects, ParsingFailedStatus, scout, Status, StatusSeverity, StatusType, strings, ValidationFailedStatus, ValueFieldEventMap, ValueFieldModel + AbstractLayout, App, aria, arrays, EnumObject, focusUtils, FormField, InitModelOf, objects, ParsingFailedStatus, scout, Status, StatusSeverity, StatusType, strings, ValidationFailedStatus, ValueFieldEventMap, ValueFieldModel } from '../../index'; import $ from 'jquery'; @@ -29,6 +29,8 @@ export class ValueField extend parser: ValueFieldParser; value: TValue; validators: ValueFieldValidator[]; + validatePending: boolean; + protected _validatePendingCounter: number; protected _updateDisplayTextPending: boolean; constructor() { @@ -45,6 +47,7 @@ export class ValueField extend this.value = null; this.validators = []; this.validators.push(this._validateValue.bind(this)); + this._validatePendingCounter = 0; this._updateDisplayTextPending = false; this.$clearIcon = null; @@ -53,11 +56,11 @@ export class ValueField extend static Clearable = { /** - * The clear icon is showed when the field has text. + * The clear icon is shown when the field has text. */ ALWAYS: 'always', /** - * The clear icon will be showed when the field is focused and has text. + * The clear icon will be shown when the field is focused and has text. */ FOCUSED: 'focused', /** @@ -150,26 +153,36 @@ export class ValueField extend // executes this._setDisplayText(), which updates the value. this._setProperty('displayText', displayText); if (!whileTyping) { - this.parseAndSetValue(displayText); + let result = this.parseAndSetValue(displayText); + if (objects.isPromise(result)) { + return result.then(() => this._triggerAcceptInput(whileTyping)); + } } // Display text may be formatted -> Use this.displayText this._triggerAcceptInput(whileTyping); } } - parseAndSetValue(displayText: string) { + /** + * Parses the text using {@link parseValue} and sets the result using {@link setValue}. + * + * @returns `void` or a promise if an asynchronous validator has been called (see {@link ValueFieldModel.validators}) + */ + parseAndSetValue(displayText: string): JQuery.Promise | void { this.removeErrorStatus(ParsingFailedStatus); + let parsedValue; try { let event = this.trigger('parse', { displayText: displayText }); if (!event.defaultPrevented) { - let parsedValue = this.parseValue(displayText); - this.setValue(parsedValue); + parsedValue = this.parseValue(displayText); } } catch (error) { this._parsingFailed(displayText, error); + return; } + return this.setValue(parsedValue); } protected _parsingFailed(displayText: string, error: any) { @@ -341,18 +354,35 @@ export class ValueField extend this.trigger('clear'); } - /** @see ValueFieldModel.value */ - setValue(value: TValue | TModelValue) { + /** + * Sets a new value if it is valid. + * + * The value is first converted to the expected type using {@link _ensureValue}, if supported by the field. + * It is then validated using {@link validateValue}. + * If the value is valid, it will be formatted using {@link formatValue} which will set the result as {@link displayText}. + * If it is not valid, a {@link ValidationFailedStatus} is added using {@link addErrorStatus}. + * Finally, a `propertyChange:value` event will be emitted. + * + * @returns `void` or a promise if an asynchronous validator has been called (see {@link ValueFieldModel.validators}) + * @see ValueFieldModel.value + **/ + setValue(value: TValue | TModelValue): JQuery.Promise | void { // Same code as in Widget#setProperty expect for the equals check // -> _setValue has to be called even if the value is equal so that update display text will be executed value = this._prepareProperty('value', value); if (this.rendered) { this._callRemoveProperty('value'); } - this._callSetProperty('value', value); - if (this.rendered) { - this._callRenderProperty('value'); + let result = this._setValue(value); + let renderValue = () => { + if (this.rendered) { + this._callRenderProperty('value'); + } + }; + if (objects.isPromise(result)) { + return result.then(error => renderValue()); } + renderValue(); } /** @@ -371,14 +401,10 @@ export class ValueField extend return value as TValue; } - protected _setValue(value: TValue | TModelValue) { - // When widget is initialized with a given errorStatus and a value -> don't remove the error - // status. This is a typical case for Scout Classic: field has a ParsingFailedError and user - // hits reload. - if (this.initialized) { - this.removeErrorStatus(ParsingFailedStatus); - this.removeErrorStatus(ValidationFailedStatus); - } + /** + * @returns `void` or a promise if an asynchronous validator has been called (see {@link ValueFieldModel.validators}) + */ + protected _setValue(value: TValue | TModelValue): JQuery.Promise | void { let oldValue = this.value; let typedValue = null; try { @@ -388,58 +414,71 @@ export class ValueField extend return; } + let valueOrPromise: TValue | JQuery.Promise; try { - this.value = this.validateValue(typedValue); + valueOrPromise = this.validateValue(typedValue); } catch (error) { this._validationFailed(typedValue, error); return; } - this._updateDisplayText(); - if (this._valueEquals(oldValue, this.value)) { - return; - } - - this._valueChanged(); - this._updateMenus(); - this._updateEmpty(); - this.updateSaveNeeded(); - this.triggerPropertyChange('value', oldValue, this.value); - } - - protected _valueEquals(valueA: TValue, valueB: TValue): boolean { - if (Array.isArray(valueA) && Array.isArray(valueB)) { - return arrays.equals(valueA, valueB); + if (objects.isPromise(valueOrPromise)) { + this._setValidatePending(true); + valueOrPromise.then( + validatedValue => { + if (this.destroyed) { + return; + } + if (this._validatePendingCounter === 1) { + this._validationSucceeded(validatedValue, oldValue); + } + this._setValidatePending(false); + }, + // Don't use catch() to not catch errors that happen in _validationSucceeded to make sure it behaves the same as the synchronous implementation + error => { + if (this.destroyed) { + return; + } + if (this._validatePendingCounter === 1) { + this._validationFailed(typedValue, error); + } + this._setValidatePending(false); + } + ).catch(error => { + // Error in succeeded or failed + // Just calling errorHandler.handle() does not create fatal errors if error is a string or a Status + let errorHandler = App.get().errorHandler; + errorHandler.analyzeError(error).then(errorInfo => errorHandler.handleErrorInfo({ + ...errorInfo, + showAsFatalError: true + })); + }); + return this.when('propertyChange:validatePending').then(() => undefined); } - return objects.equals(valueA, valueB); + this._validationSucceeded(valueOrPromise, oldValue); } - /** - * Is called after a value is changed. May be implemented by subclasses. The default does nothing. - */ - protected _valueChanged() { - // NOP - } - - protected override _getCurrentMenuTypes(): string[] { - if (objects.isNullOrUndefined(this.value)) { - return [...super._getCurrentMenuTypes(), ValueField.MenuType.Null]; - } - return [...super._getCurrentMenuTypes(), ValueField.MenuType.NotNull]; + protected _setValidatePending(validatePending: boolean) { + let counter = Math.max(validatePending ? this._validatePendingCounter + 1 : this._validatePendingCounter - 1, 0); + this._validatePendingCounter = counter; + this._setProperty('validatePending', counter > 0); } /** - * Validates the value by executing the validators. If a new value is the result, it will be set. + * Validates the value by executing the {@link validators}. If a new value is the result, it will be set. + * + * @returns `void` or a promise if an asynchronous validator has been called (see {@link ValueFieldModel.validators}) */ - validate() { - this._setValue(this.value); + validate(): JQuery.Promise | void { + return this._setValue(this.value); } /** * @param validator the validator to be added. * A validator is a function that accepts a raw value and either returns the validated value or - * throws an Error, a Status or an error message (string) if the value is invalid. + * throws an {@link Error}, a {@link Status} or an error message as `string` if the value is invalid. * @param revalidate True, to revalidate the value, false to just add the validator and do nothing else. Default is true. + * @see ValueFieldModel.validators */ addValidator(validator: ValueFieldValidator, revalidate?: boolean) { let validators = this.validators.slice(); @@ -450,6 +489,7 @@ export class ValueField extend /** * @param validator the validator to be removed * @param revalidate True, to revalidate the value, false to just remove the validator and do nothing else. Default is true. + * @see ValueFieldModel.validators */ removeValidator(validator: ValueFieldValidator, revalidate?: boolean) { let validators = this.validators.slice(); @@ -458,13 +498,14 @@ export class ValueField extend } /** - * Replaces all existing validators with the given one. If you want to add multiple validators, use {@link #addValidator}. - *

+ * Replaces all existing validators with the given one. + * If you want to add multiple validators, use {@link addValidator}. + * * Remember calling the default validator which is passed as parameter to the validate function, if needed. * * @param validator the new validator which replaces every other. If null, the default validator is used. * A validator is a function that accepts a raw value and either returns the validated value or - * throws an Error, a Status or an error message (string) if the value is invalid. + * throws an {@link Error}, a {@link Status} or an error message as `string` if the value is invalid. */ setValidator(validator: ValueFieldValidator, revalidate?: boolean) { if (!validator) { @@ -477,6 +518,7 @@ export class ValueField extend this.setValidators(validators, revalidate); } + /** @see ValueFieldModel.validators */ setValidators(validators: ValueFieldValidator[], revalidate?: boolean) { this.setProperty('validators', validators); if (this.initialized && scout.nvl(revalidate, true)) { @@ -485,24 +527,39 @@ export class ValueField extend } /** - * @param the value to be validated - * @returns the validated value - * @throws a message, a {@link Status} or an error if the validation fails + * Validates the given value using the {@link ValueFieldModel.validators}. + * + * @param value the value to be validated + * @returns the validated value or a promise if an asynchronous validator has been called (see {@link ValueFieldModel.validators}) + * @throws a message, a {@link Status} or an {@link Error} if the validation fails */ - validateValue(value: TValue): TValue { + validateValue(value: TValue): TValue | JQuery.Promise { let defaultValidator = this._validateValue.bind(this); - this.validators.forEach(validator => { - value = validator(value, defaultValidator); - }); - value = scout.nvl(value, null); // Ensure value is never undefined (necessary for updateSaveNeeded and should make it easier generally) + + // Ensure value is never undefined (necessary for updateSaveNeeded and should make it easier in general) + let ensureNotUndefined = v => scout.nvl(v, null); + let validators = [...this.validators, ensureNotUndefined]; + + return this._validateValueImpl(value, validators, defaultValidator); + } + + protected _validateValueImpl(value: TValue, validators: ValueFieldValidator[], defaultValidator: ValueFieldValidator): TValue | JQuery.Promise { + for (let i = 0; i < validators.length; i++) { + const validator = validators[i]; + const valueOrPromise = validator(value, defaultValidator); + if (objects.isPromise(valueOrPromise)) { + return valueOrPromise.then(result => this._validateValueImpl(result, validators.slice(i + 1), defaultValidator)); + } + value = valueOrPromise as TValue; + } return value; } /** - * @returns the validated value - * @throws a message, a {@link Status} or an error if the validation fails + * @returns the validated value or a promise if the validation happens asynchronously + * @throws a message, a {@link Status} or an {@link Error} if the validation fails */ - protected _validateValue(value: TValue): TValue { + protected _validateValue(value: TValue): TValue | JQuery.Promise { if (typeof value === 'string' && value === '') { // Convert empty string to null. // Not using strings.nullIfEmpty is by purpose because it also removes white space characters which may not be desired here @@ -511,8 +568,45 @@ export class ValueField extend return value; } + protected _validationSucceeded(newValue: TValue, oldValue: TValue) { + // When widget is initialized with a given errorStatus and a value -> don't remove the error + // status. This is a typical case for Scout Classic: field has a ParsingFailedError and user + // hits reload. + if (this.initialized) { + this.removeErrorStatus(ParsingFailedStatus); + this.removeErrorStatus(ValidationFailedStatus); + } + this.value = newValue; + this._updateDisplayText(); + if (this._valueEquals(oldValue, this.value)) { + return; + } + + this._valueChanged(); + this._updateMenus(); + this._updateEmpty(); + this.updateSaveNeeded(); + this.triggerPropertyChange('value', oldValue, this.value); + } + + protected _valueEquals(valueA: TValue, valueB: TValue): boolean { + if (Array.isArray(valueA) && Array.isArray(valueB)) { + return arrays.equals(valueA, valueB); + } + return objects.equals(valueA, valueB); + } + + /** + * Is called after a value is changed. May be implemented by subclasses. The default does nothing. + */ + protected _valueChanged() { + // NOP + } + protected _validationFailed(value: TValue, error: any) { $.log.isDebugEnabled() && $.log.debug('Validation failed for field with id ' + this.id, error); + this.removeErrorStatus(ParsingFailedStatus); + this.removeErrorStatus(ValidationFailedStatus); let status = this._createValidationFailedStatus(value, error); this.addErrorStatus(status); this._updateDisplayText(value); @@ -520,6 +614,8 @@ export class ValueField extend protected _ensureValueFailed(value: TModelValue, error: any) { $.log.isDebugEnabled() && $.log.debug('EnsureValue failed for field with id ' + this.id, error); + this.removeErrorStatus(ParsingFailedStatus); + this.removeErrorStatus(ValidationFailedStatus); let status = this._createValidationFailedStatus(value, error); this.addErrorStatus(status); this.setDisplayText(this._formatRawValue(value)); @@ -662,6 +758,13 @@ export class ValueField extend return this.value === null || this.value === undefined || (Array.isArray(this.value) && arrays.empty(this.value)); } + protected override _getCurrentMenuTypes(): string[] { + if (objects.isNullOrUndefined(this.value)) { + return [...super._getCurrentMenuTypes(), ValueField.MenuType.Null]; + } + return [...super._getCurrentMenuTypes(), ValueField.MenuType.NotNull]; + } + // ==== static helper methods ==== // /** @@ -704,6 +807,6 @@ export class ValueField extend export type ValueFieldClearable = EnumObject; export type ValueFieldMenuType = EnumObject; -export type ValueFieldValidator = (value: TValue, defaultValidator?: ValueFieldValidator) => TValue; +export type ValueFieldValidator = (value: TValue, defaultValidator?: ValueFieldValidator) => TValue | JQuery.Promise; export type ValueFieldFormatter = (value: TValue, defaultFormatter?: ValueFieldFormatter) => string | JQuery.Promise; export type ValueFieldParser = (displayText: string, defaultParser?: ValueFieldParser) => TValue; diff --git a/eclipse-scout-core/src/form/fields/ValueFieldEventMap.ts b/eclipse-scout-core/src/form/fields/ValueFieldEventMap.ts index 5c2e0d93845..05ecaf5881f 100644 --- a/eclipse-scout-core/src/form/fields/ValueFieldEventMap.ts +++ b/eclipse-scout-core/src/form/fields/ValueFieldEventMap.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -29,6 +29,7 @@ export interface ValueFieldEventMap extends FormFieldEventMap { 'parse': ValueFieldParseEvent; 'parseError': ValueFieldParseErrorEvent; 'propertyChange:value': PropertyChangeEvent; + 'propertyChange:validatePending': PropertyChangeEvent; 'propertyChange:clearable': PropertyChangeEvent; 'propertyChange:displayText': PropertyChangeEvent; 'propertyChange:formatter': PropertyChangeEvent>; diff --git a/eclipse-scout-core/src/form/fields/ValueFieldModel.ts b/eclipse-scout-core/src/form/fields/ValueFieldModel.ts index cffcb52570f..dc2717524a2 100644 --- a/eclipse-scout-core/src/form/fields/ValueFieldModel.ts +++ b/eclipse-scout-core/src/form/fields/ValueFieldModel.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -16,6 +16,9 @@ export interface ValueFieldModel[]; @@ -49,6 +52,9 @@ export interface ValueFieldModel implements DateFi return dates.ensure(value); } - protected override _validateValue(value: Date): Date { + protected override _validateValue(value: Date): Date | JQuery.Promise { if (objects.isNullOrUndefined(value)) { return value; } diff --git a/eclipse-scout-core/src/form/fields/filechooserbutton/FileChooserButton.ts b/eclipse-scout-core/src/form/fields/filechooserbutton/FileChooserButton.ts index d9c739ba056..38a68ef6a97 100644 --- a/eclipse-scout-core/src/form/fields/filechooserbutton/FileChooserButton.ts +++ b/eclipse-scout-core/src/form/fields/filechooserbutton/FileChooserButton.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -140,7 +140,7 @@ export class FileChooserButton extends ValueField implements FileChooserBu this.setValue(arrays.first(event.files)); } - protected override _validateValue(value: File): File { + protected override _validateValue(value: File): File | JQuery.Promise { this.fileInput.validateMaximumUploadSize(value); return value; } diff --git a/eclipse-scout-core/src/form/fields/filechooserfield/FileChooserField.ts b/eclipse-scout-core/src/form/fields/filechooserfield/FileChooserField.ts index d84135ae4cf..a1abe0aa1bf 100644 --- a/eclipse-scout-core/src/form/fields/filechooserfield/FileChooserField.ts +++ b/eclipse-scout-core/src/form/fields/filechooserfield/FileChooserField.ts @@ -135,7 +135,7 @@ export class FileChooserField extends ValueField implements FileChooserFie this.fileInput.browse(); } - protected override _validateValue(value: File): File { + protected override _validateValue(value: File): File | JQuery.Promise { this.fileInput.validateMaximumUploadSize(value); return value; } diff --git a/eclipse-scout-core/src/form/fields/numberfield/NumberField.ts b/eclipse-scout-core/src/form/fields/numberfield/NumberField.ts index 40e7eef176a..e8894ebfc4f 100644 --- a/eclipse-scout-core/src/form/fields/numberfield/NumberField.ts +++ b/eclipse-scout-core/src/form/fields/numberfield/NumberField.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -153,7 +153,7 @@ export class NumberField extends BasicField implements return typedValue; } - protected override _validateValue(value: number): number { + protected override _validateValue(value: number): number | JQuery.Promise { if (objects.isNullOrUndefined(value)) { return value; } diff --git a/eclipse-scout-core/src/form/fields/radiobutton/RadioButtonGroup.ts b/eclipse-scout-core/src/form/fields/radiobutton/RadioButtonGroup.ts index d5a8e00df46..97305b9d951 100644 --- a/eclipse-scout-core/src/form/fields/radiobutton/RadioButtonGroup.ts +++ b/eclipse-scout-core/src/form/fields/radiobutton/RadioButtonGroup.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -258,7 +258,7 @@ export class RadioButtonGroup extends ValueField implements Radi /** * Search and then select the button with the corresponding radioValue */ - protected override _validateValue(value: TValue): TValue { + protected override _validateValue(value: TValue): TValue | JQuery.Promise { super._validateValue(value); if (!this.initialized && this.lookupCall) { diff --git a/eclipse-scout-core/src/form/fields/sliderfield/SliderField.ts b/eclipse-scout-core/src/form/fields/sliderfield/SliderField.ts index 002e6d984f0..4a2911944b4 100644 --- a/eclipse-scout-core/src/form/fields/sliderfield/SliderField.ts +++ b/eclipse-scout-core/src/form/fields/sliderfield/SliderField.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -143,7 +143,7 @@ export class SliderField extends NumberField implements SliderFieldModel { } } - protected override _validateValue(value: number): number { + protected override _validateValue(value: number): number | JQuery.Promise { return super._validateValue(scout.nvl(value, this.minValue)); } diff --git a/eclipse-scout-core/src/form/fields/smartfield/ProposalField.ts b/eclipse-scout-core/src/form/fields/smartfield/ProposalField.ts index cee294b220f..a2a2521e836 100644 --- a/eclipse-scout-core/src/form/fields/smartfield/ProposalField.ts +++ b/eclipse-scout-core/src/form/fields/smartfield/ProposalField.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -83,7 +83,7 @@ export class ProposalField extends SmartField implements ProposalFieldMo return value; } - protected override _validateValue(value: string): string { + protected override _validateValue(value: string): string | JQuery.Promise { if (objects.isNullOrUndefined(value)) { return value; } diff --git a/eclipse-scout-core/src/form/fields/stringfield/StringField.ts b/eclipse-scout-core/src/form/fields/stringfield/StringField.ts index 6086f1c708e..07c8d7fb1d2 100644 --- a/eclipse-scout-core/src/form/fields/stringfield/StringField.ts +++ b/eclipse-scout-core/src/form/fields/stringfield/StringField.ts @@ -534,7 +534,7 @@ export class StringField extends BasicField implements StringFieldModel }); } - protected override _validateValue(value: string): string { + protected override _validateValue(value: string): string | JQuery.Promise { if (objects.isNullOrUndefined(value)) { return value; } @@ -556,14 +556,14 @@ export class StringField extends BasicField implements StringFieldModel return strings.empty(this.value); } - override acceptInput(whileTyping?: boolean) { + override acceptInput(whileTyping?: boolean): JQuery.Promise | void { let displayText = scout.nvl(this._readDisplayText(), ''); if (this.inputObfuscated && displayText !== '') { // Disable obfuscation if user has typed text (on focus, field will be cleared if obfuscated, so any typed text is new text). this.inputObfuscated = false; } - super.acceptInput(whileTyping); + return super.acceptInput(whileTyping); } protected override _onFieldFocus(event: JQuery.FocusEvent) { diff --git a/eclipse-scout-core/src/form/fields/tagfield/TagField.ts b/eclipse-scout-core/src/form/fields/tagfield/TagField.ts index 53c90f1f8e2..4d2513a4d7f 100644 --- a/eclipse-scout-core/src/form/fields/tagfield/TagField.ts +++ b/eclipse-scout-core/src/form/fields/tagfield/TagField.ts @@ -119,7 +119,7 @@ export class TagField extends ValueField implements TagFieldModel { return ''; } - protected override _validateValue(value: string[]): string[] { + protected override _validateValue(value: string[]): string[] | JQuery.Promise { let tags = arrays.ensure(value); let result: string[] = []; tags.forEach(tag => { diff --git a/eclipse-scout-core/src/form/lifecycle/FormLifecycle.ts b/eclipse-scout-core/src/form/lifecycle/FormLifecycle.ts index aaa089b74bf..d47b56c7a6f 100644 --- a/eclipse-scout-core/src/form/lifecycle/FormLifecycle.ts +++ b/eclipse-scout-core/src/form/lifecycle/FormLifecycle.ts @@ -48,12 +48,16 @@ export class FormLifecycle(widget: Form | FormField): ElementsValidationResult { const missingElements = []; const invalidElements = []; + const pendingElements = []; widget.visitFields((field: FormField) => { let result = field.getValidationResult(); if (!result.valid) { - // error status has priority over mandatory - if (result.errorStatus && result.errorStatus.isError()) { // ERROR + if (result.promise) { + // async validation is still pending + pendingElements.push(result); + } else if (result.errorStatus && result.errorStatus.isError()) { // ERROR + // errorStatus has priority over mandatory invalidElements.push(result); } else if (!result.validByMandatory) { // empty mandatory missingElements.push(result); @@ -67,7 +71,7 @@ export class FormLifecycle extends EventEmitter implements LifecycleModel, ObjectWithType { +export abstract class Lifecycle }> extends EventEmitter implements LifecycleModel, ObjectWithType { declare model: LifecycleModel; declare initModel: SomeRequired; declare eventMap: LifecycleEventMap; @@ -243,14 +243,14 @@ export abstract class Lifecycle { - let elementStatus = this._validateElements(); - if (elementStatus.isError()) { - return $.resolvedPromise(elementStatus); - } - - const widgetValidation = this._validateWidget(); - const promise = objects.isPromise(widgetValidation) ? widgetValidation : $.resolvedPromise(widgetValidation); - return promise.then(widgetStatus => this._combineValidationStatuses(elementStatus, widgetStatus)); + return this._validateElements().then(elementStatus => { + if (elementStatus.isError()) { + return elementStatus; + } + const widgetValidation = this._validateWidget(); + const promise = objects.isPromise(widgetValidation) ? widgetValidation : $.resolvedPromise(widgetValidation); + return promise.then(widgetStatus => this._combineValidationStatuses(elementStatus, widgetStatus)); + }); } protected _combineValidationStatuses(elementStatus: Status, widgetStatus: Status): Status { @@ -260,19 +260,24 @@ export abstract class Lifecycle { + const validationResult = this.invalidElements(); + if (validationResult.pendingElements.length > 0) { + return $.promiseAll(validationResult.pendingElements.map(element => element.promise)) + .then(() => this._validateElements()); + } + let severity: StatusSeverity; let message: string; - if (elementsValidationResult.missingElements.length === 0 && elementsValidationResult.invalidElements.length === 0) { + if (validationResult.missingElements.length === 0 && validationResult.invalidElements.length === 0) { severity = Status.Severity.OK; } else { - severity = elementsValidationResult.missingElements.length + severity = validationResult.missingElements.length ? Status.Severity.ERROR - : arrays.max(elementsValidationResult.invalidElements.map(e => e.errorStatus ? e.errorStatus.severity : 0)) as StatusSeverity; - message = this._createInvalidElementsMessageHtml(elementsValidationResult.missingElements, elementsValidationResult.invalidElements); + : arrays.max(validationResult.invalidElements.map(e => e.errorStatus ? e.errorStatus.severity : 0)) as StatusSeverity; + message = this._createInvalidElementsMessageHtml(validationResult.missingElements, validationResult.invalidElements); } - return scout.create(ElementsValidationStatus, {severity, message, elementsValidationResult}); + return $.resolvedPromise(scout.create(ElementsValidationStatus, {severity, message, elementsValidationResult: validationResult})); } protected _revealInvalidElement(invalidElement: TValidationResult) { @@ -295,7 +300,8 @@ export abstract class Lifecycle { return { missingElements: [], - invalidElements: [] + invalidElements: [], + pendingElements: [] }; } @@ -395,5 +401,14 @@ export abstract class Lifecycle = { missingElements: TValidationResult[]; invalidElements: TValidationResult[] }; -export type TextListWithTitle = { title: string; elements: string[]; html?: boolean }; +export type ElementsValidationResult = { + missingElements: TValidationResult[]; + invalidElements: TValidationResult[]; + pendingElements: TValidationResult[]; +}; + +export type TextListWithTitle = { + title: string; + elements: string[]; + html?: boolean; +}; diff --git a/eclipse-scout-core/test/form/FormSpec.ts b/eclipse-scout-core/test/form/FormSpec.ts index 9a0dd097617..7167890d0a6 100644 --- a/eclipse-scout-core/test/form/FormSpec.ts +++ b/eclipse-scout-core/test/form/FormSpec.ts @@ -246,6 +246,24 @@ describe('Form', () => { await form.load(); expect(loadCounter).toBe(1); }); + + it('waits for async field validators to complete', async () => { + let form = helper.createFormWithOneField(); + let field = form.rootGroupBox.fields[0] as StringField; + + field.setValidators([value => sleep(50).then(() => value + ' async')], false); + field.setValue('x'); + + let promise = form.load(); + expect(field.value).toBe(null); + expect(field.validatePending).toBe(true); + + await promise; + expect(field.value).toBe('x async'); + expect(field.validatePending).toBe(false); + + return promise; + }); }); describe('save', () => { @@ -1987,7 +2005,6 @@ describe('Form', () => { }); describe('validate', () => { - let form: SpecForm; let mandatoryStringField: StringField; let numberField: NumberField; @@ -2151,6 +2168,68 @@ describe('Form', () => { jasmine.clock().uninstall(); }); + + it('waits for all async field validators to complete', async () => { + mandatoryStringField.setMandatory(false); + + numberField.setValidators([value => sleep(50).then(() => value + 1), value => $.resolvedPromise(value + 1)], false); + numberField.setValue(1); + + let promise = form.validate(); + expect(numberField.value).toBe(null); + expect(numberField.validatePending).toBe(true); + + let status = await promise; + expect(numberField.value).toBe(3); + expect(numberField.validatePending).toBe(false); + expect(status.isValid()).toBeTrue(); + + return promise; + }); + + it('waits for all async field validators to complete and returns false if one is invalid', async () => { + mandatoryStringField.setMandatory(false); + + numberField.setValidators([value => sleep(50).then(() => value + 1), value => { + throw 'wrong number'; + }], false); + numberField.setValue(1); + + let promise = form.validate(); + expect(numberField.value).toBe(null); + + session.desktop.when('propertyChange:messageBoxes').then(() => helper.closeMessageBoxes()); + let status = await promise; + expect(numberField.value).toBe(null); + expect(numberField.validatePending).toBe(false); + expect(numberField.errorStatus.message).toBe('wrong number'); + expect(status.isValid()).toBeFalse(); + + return promise; + }); + + it('waits for all async field validators to complete if a field removes its error', async () => { + mandatoryStringField.setMandatory(false); + + numberField.setValue('invalid number'); + expect(numberField.errorStatus).toBeInstanceOf(Status); + + numberField.setValidators([value => sleep(50).then(() => value + 1), value => $.resolvedPromise(value + 1)], false); + numberField.setValue(1); + + let promise = form.validate(); + expect(numberField.value).toBe(null); + expect(numberField.validatePending).toBe(true); + expect(numberField.errorStatus).toBeInstanceOf(Status); + + let status = await promise; + expect(numberField.value).toBe(3); + expect(numberField.validatePending).toBe(false); + expect(numberField.errorStatus).toBe(null); + expect(status.isValid()).toBeTrue(); + + return promise; + }); }); describe('error', () => { diff --git a/eclipse-scout-core/test/form/fields/ValueFieldSpec.ts b/eclipse-scout-core/test/form/fields/ValueFieldSpec.ts index 469055224b3..4f404a610d0 100644 --- a/eclipse-scout-core/test/form/fields/ValueFieldSpec.ts +++ b/eclipse-scout-core/test/form/fields/ValueFieldSpec.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -7,7 +7,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -import {arrays, FormField, ParsingFailedStatus, scout, Status, StringField, ValueField} from '../../../src/index'; +import {ajax, AjaxCall, App, arrays, FormField, NumberField, ParsingFailedStatus, scout, Status, StringField, ValueField} from '../../../src/index'; import {FormSpecHelper, MenuSpecHelper} from '../../../src/testing/index'; import {ValueFieldValidator} from '../../../src/form/fields/ValueField'; @@ -181,6 +181,15 @@ describe('ValueField', () => { }); describe('setValue', () => { + class SpecStringField extends StringField { + override _validationSucceeded(newValue: string, oldValue: string) { + super._validationSucceeded(newValue, oldValue); + } + + override _validationFailed(value: string, error: any) { + super._validationFailed(value, error); + } + } it('sets the value, formats it and sets the display text', () => { let field = helper.createField(StringField); @@ -273,6 +282,227 @@ describe('ValueField', () => { expect(field.displayText).toBe(''); }); + it('waits for the async validators to complete before removing the error status', async () => { + jasmine.clock().uninstall(); + let field = helper.createField(NumberField); + field.setValue('invalid number'); + expect(field.errorStatus).toBeInstanceOf(Status); + + field.addValidator(value => $.resolvedPromise(value)); + field.setValue(3); + expect(field.value).toBe(null); + expect(field.errorStatus).toBeInstanceOf(Status); + + await field.when('propertyChange:validatePending'); + expect(field.value).toBe(3); + expect(field.errorStatus).toBe(null); + }); + + it('only processes the success result of the last async validator if the previous ones have been aborted', async () => { + jasmine.clock().uninstall(); + let field = scout.create(SpecStringField, { + parent: session.desktop + }); + spyOn(field, '_validationSucceeded').and.callThrough(); + spyOn(field, '_validationFailed').and.callThrough(); + + let call: AjaxCall; + field.addValidator(value => { + call?.abort(); + call = ajax.createCall({ + url: 'validate', + method: 'POST', + data: value, + dataType: 'text' + }); + return call.call().then(response => response); + }, false); + field.setValue('x'); + expect(field.value).toBe(null); + + field.setValue('y'); + let request = jasmine.Ajax.requests.mostRecent(); + request.respondWith({ + status: 200, + responseText: request.params + ' validated' + }); + await field.when('propertyChange:validatePending'); + expect(field.value).toBe('y validated'); + expect(field.displayText).toBe('y validated'); + expect(field._validationSucceeded).toHaveBeenCalledTimes(1); + expect(field._validationFailed).toHaveBeenCalledTimes(0); + }); + + it('only processes the error result of the last async validator if the previous ones have been aborted', async () => { + jasmine.clock().uninstall(); + + let field = scout.create(SpecStringField, { + parent: session.desktop + }); + spyOn(field, '_validationSucceeded').and.callThrough(); + spyOn(field, '_validationFailed').and.callThrough(); + + let call: AjaxCall; + field.addValidator(value => { + call?.abort(); + call = ajax.createCall({ + url: 'validate', + method: 'POST', + data: value, + dataType: 'text' + }); + return call.call().then(response => response); + }, false); + field.setValue('x'); + expect(field.value).toBe(null); + expect(field.errorStatus).toBe(null); + + field.setValue('y'); + let request = jasmine.Ajax.requests.mostRecent(); + request.respondWith({ + status: 500, + responseText: request.params + ' validated' + }); + await field.when('propertyChange:validatePending'); + expect(field.value).toBe(null); + expect(field.displayText).toBe('y'); + expect(field.errorStatus.message).toBe('[undefined text: InvalidValueMessageX]'); + expect(field._validationSucceeded).toHaveBeenCalledTimes(0); + expect(field._validationFailed).toHaveBeenCalledTimes(1); + }); + + it('does nothing when validator succeeds but field is destroyed', () => { + let field = helper.createField(SpecStringField); + spyOn(field, '_validationSucceeded').and.callThrough(); + spyOn(field, '_validationFailed').and.callThrough(); + + field.addValidator(value => $.resolvedPromise(value)); + field.setValue('x'); + expect(field.value).toBe(null); + expect(field.errorStatus).toBe(null); + + field.destroy(); + jasmine.clock().tick(1); + expect(field.value).toBe(null); + expect(field.errorStatus).toBe(null); + expect(field._validationSucceeded).not.toHaveBeenCalled(); + expect(field._validationFailed).not.toHaveBeenCalled(); + }); + + it('does nothing when validator fails but field is destroyed', async () => { + let field = helper.createField(SpecStringField); + spyOn(field, '_validationSucceeded').and.callThrough(); + spyOn(field, '_validationFailed').and.callThrough(); + + field.addValidator(value => { + return $.resolvedPromise(value).then(() => { + throw 'fail'; + }); + }); + field.setValue('x'); + expect(field.value).toBe(null); + expect(field.errorStatus).toBe(null); + + field.destroy(); + jasmine.clock().tick(1); + expect(field.value).toBe(null); + expect(field.errorStatus).toBe(null); + expect(field._validationSucceeded).not.toHaveBeenCalled(); + expect(field._validationFailed).not.toHaveBeenCalled(); + }); + }); + + describe('validateValue', () => { + it('validates the value using the validators', () => { + let field = scout.create(StringField, { + parent: session.desktop + }); + + field.setValidators([ + value => { + if (value === 'a') { + throw 'a is not allowed'; + } + if (value === 'aa') { + return 'aaa'; + } + return value; + }, + value => { + if (value === 'b') { + throw 'b is not allowed'; + } + if (value === 'bb') { + return 'bbb'; + } + return value; + } + ]); + expect(field.validateValue('x')).toBe('x'); + expect(field.validateValue('aa')).toBe('aaa'); + expect(field.validateValue('bb')).toBe('bbb'); + expect(() => field.validateValue('a')).toThrow('a is not allowed'); + expect(() => field.validateValue('b')).toThrow('b is not allowed'); + }); + + it('can handle async validators', async () => { + jasmine.clock().uninstall(); + let field = scout.create(StringField, { + parent: session.desktop + }); + + field.setValidators([ + value => { + if (value === 'a') { + throw 'a is not allowed'; + } + if (value === 'aa') { + return 'aaa'; + } + return value; + }, + value => { + const def = $.Deferred(); + setTimeout(() => { + if (value === 'b') { + def.reject('b is not allowed'); + } + def.resolve(value); + }); + return def.promise(); + }, + value => { + const def = $.Deferred(); + setTimeout(() => { + if (value === 'c') { + def.reject(Status.error('c is not allowed')); + } + if (value === 'cc') { + def.resolve('ccc'); + } + def.resolve(value); + }); + return def.promise(); + }, + value => { + if (value === 'd') { + throw Status.error('d is not allowed'); + } + if (value === 'dd') { + return 'ddd'; + } + return value; + } + ], false); + expect(await field.validateValue('x')).toBe('x'); + expect(await field.validateValue('aa')).toBe('aaa'); + expect(await field.validateValue('cc')).toBe('ccc'); + expect(await field.validateValue('dd')).toBe('ddd'); + expect(() => field.validateValue('a')).toThrow('a is not allowed'); + await expectAsync(field.validateValue('b')).toBeRejectedWith('b is not allowed'); + await expectAsync(field.validateValue('c')).toBeRejectedWith(Status.error('c is not allowed')); + await expectAsync(field.validateValue('d')).toBeRejectedWith(Status.error('d is not allowed')); + }); }); describe('_validateValue', () => { @@ -320,6 +550,19 @@ describe('ValueField', () => { expect(field.value).toBe('Foo'); }); + it('returns a promise if an async validator is used', async () => { + jasmine.clock().uninstall(); + let field = helper.createField(StringField); + field.addValidator(value => $.resolvedPromise(value += ' async')); + let promise = field.parseAndSetValue('Foo'); + expect(field.displayText).toBe(''); + expect(field.value).toBe(null); + + await promise; + expect(field.displayText).toBe('Foo async'); + expect(field.value).toBe('Foo async'); + }); + it('does not set the value but the error status if the parsing fails', () => { let field = helper.createField(StringField); field.setParser(text => { @@ -347,6 +590,29 @@ describe('ValueField', () => { expect(field.errorStatus).toBe(null); }); + it('does not catch errors that are not caught by setValue', () => { + let field = helper.createField(StringField); + field.on('propertyChange:value', () => { + throw 'error in value change'; + }); + // Only errors that happen during the actual validating (validators) or parsing (parser) should result in an error status + // A value change listener could do anything leaving the application in an unexpected state if it fails + expect(() => field.parseAndSetValue('Foo')).toThrow(); + expect(field.errorStatus).toBe(null); + }); + + it('does not catch async errors that are not caught by setValue', () => { + let field = helper.createField(StringField); + field.addValidator(value => $.resolvedPromise(value += ' async')); + field.on('propertyChange:value', () => { + throw 'error in value change'; + }); + spyOn(App.get().errorHandler, 'handleErrorInfo'); + field.parseAndSetValue('Foo'); + jasmine.clock().tick(1); + expect(App.get().errorHandler.handleErrorInfo).toHaveBeenCalled(); + expect(field.errorStatus).toBe(null); + }); }); describe('acceptInput', () => { @@ -382,6 +648,23 @@ describe('ValueField', () => { expect(displayText).toBe('a value'); }); + it('is triggered later if there is an async validator', async () => { + jasmine.clock().uninstall(); + let field = helper.createField(StringField); + let displayText; + field.render(); + field.on('acceptInput', event => { + displayText = event.displayText; + }); + field.addValidator(value => $.resolvedPromise(value += ' async')); + field.$field.val('a value'); + let promise = field.acceptInput(); + expect(displayText).toBeUndefined(); + + await promise; + expect(displayText).toBe('a value async'); + }); + it('contains the actual displayText even if it was changed using format value', () => { let field = helper.createField(StringField); field.render(); @@ -441,7 +724,7 @@ describe('ValueField', () => { parent: session.desktop, value: 'hi', validator: (value, defaultValidator: ValueFieldValidator) => { - value = defaultValidator(value); + value = defaultValidator(value) as string; if (value === 'hi') { throw 'Hi is not allowed'; } @@ -498,7 +781,7 @@ describe('ValueField', () => { let field = helper.createField(StringField); expect(field.validators.length).toBe(1); field.setValidator((value, defaultValidator) => { - value = defaultValidator(value); + value = defaultValidator(value) as string; if (value === 'hi') { throw 'Hi is not allowed'; } @@ -676,7 +959,7 @@ describe('ValueField', () => { expect(field.empty).toBe(true); }); - it('validate returns valid when errorStatus is not set and field is not mandatory', () => { + it('validationResult.valid is true when errorStatus is not set and field is not mandatory', () => { field.setValue(null); field.setErrorStatus(null); field.setMandatory(false); @@ -684,7 +967,7 @@ describe('ValueField', () => { expect(result.valid).toBe(true); }); - it('validate returns not valid when errorStatus is set or field is mandatory and empty', () => { + it('validationResult.valid is false when errorStatus is set or field is mandatory and empty', () => { let errorStatus = new Status({ severity: Status.Severity.ERROR }); @@ -700,6 +983,19 @@ describe('ValueField', () => { expect(result.validByMandatory).toBe(false); }); + it('validationResult.valid is false when validation is pending', async () => { + jasmine.clock().uninstall(); + field.addValidator(value => $.resolvedPromise(value)); + let result = field.getValidationResult(); + expect(result.valid).toBe(false); + expect(result.promise).toBeDefined(); + + await result.promise; + result = field.getValidationResult(); + expect(result.valid).toBe(true); + expect(result.promise).toBe(null); + }); + describe('saveNeeded', () => { it('is set false initially', () => { let field = scout.create(StringField, { diff --git a/eclipse-scout-core/test/form/lifecycle/FormLifecycleSpec.ts b/eclipse-scout-core/test/form/lifecycle/FormLifecycleSpec.ts index 43053d24709..682754d8536 100644 --- a/eclipse-scout-core/test/form/lifecycle/FormLifecycleSpec.ts +++ b/eclipse-scout-core/test/form/lifecycle/FormLifecycleSpec.ts @@ -121,7 +121,8 @@ describe('FormLifecycle', () => { expect(lifecycleComplete).toBe(expected); } - it('should call _lifecycleValidate function on form', () => { + it('should call _lifecycleValidate function on form', async () => { + jasmine.clock().uninstall(); // validate should always be called, even when there is not a single touched field in the form let form2 = helper.createFormWithOneField(); form2.lifecycle = scout.create(SpecLifecycle, { @@ -132,7 +133,7 @@ describe('FormLifecycle', () => { validateCalled = true; return Status.ok(); }); - form2.ok(); + await form2.ok(); expect(validateCalled).toBe(true); // validate should not be called when there is an invalid field (field is mandatory but empty in this case) @@ -140,7 +141,8 @@ describe('FormLifecycle', () => { let formField = form2.rootGroupBox.fields[0]; formField.touch(); formField.setMandatory(true); - form2.ok(); + session.desktop.when('propertyChange:messageBoxes').then(() => helper.closeMessageBoxes()); + await form2.ok(); expect(validateCalled).toBe(false); });