From 6f89d63456133b92653af3a16f6211dd50ff51b6 Mon Sep 17 00:00:00 2001 From: Fritz Schinkel Date: Wed, 28 Feb 2024 10:31:17 +0100 Subject: [PATCH] ProposalField: fix validation errors If a potential value that is not accepted by the validator of a ProposalField is passed to the field, the display text needs to change while the value stays the same. If this value comes from a selected lookupRow, the lookupRow will be set and therefore, the formatting needs to consider it. If this value comes from a touch popup, there are several things that need to be considered: * The lookupRow must not trigger any value changed logic, as setting a value that does not pass validation will reset the value to the previous one. Otherwise, if the new lookupRow is null it will set the value of the field to null and therefore the reset-logic after a failed validation resets the value to null as the former value is already lost. * The displayText needs to be set without exception, as it can differ from the value. Consider the value 'info' for which a validator adds an error status with severity info and another value 'throw' for which the validation fails. Switching between these two values will never change the value, it will always stay 'info'. But what needs to change are the displayText and the error status. Therefore, the value needs to be set again when switching from 'throw' back to 'info' as the validator needs to run in order to add the info-error status. The displayText needs to be set to support the 'info'>'throw' case as setting a value 'throw' will never change the displayText it needs to be transferred explicitly from the touch popup. * The value needs to be set without exception, even if it is identical to the current value. See example for displayText. To support the 'throw' > 'info' case from the previous example correctly for Scout Classic, an input needs to be accepted and all listeners need to be informed if the search text changed to the text of the current lookupRow. Consider the previous example and the inputs "lookup 'info'" > "lookup 'throw'" > "write 'info'". While writing 'info', the 'info' lookupRow is still present and the searchText changes from 'throw' to 'info' and therefore, the listener in the adapter needs to be triggered in order to validate 'info' again on the ui server. 369873 --- .../form/fields/smartfield/ProposalField.js | 23 +- .../fields/smartfield/ProposalFieldSpec.js | 426 +++++++++++++++++- 2 files changed, 438 insertions(+), 11 deletions(-) diff --git a/eclipse-scout-core/src/form/fields/smartfield/ProposalField.js b/eclipse-scout-core/src/form/fields/smartfield/ProposalField.js index 4076cefe097..f41edbba272 100644 --- a/eclipse-scout-core/src/form/fields/smartfield/ProposalField.js +++ b/eclipse-scout-core/src/form/fields/smartfield/ProposalField.js @@ -8,7 +8,7 @@ * Contributors: * BSI Business Systems Integration AG - initial API and implementation */ -import {objects, scout, SmartField, strings} from '../../../index'; +import {objects, SmartField, strings} from '../../../index'; import $ from 'jquery'; export default class ProposalField extends SmartField { @@ -69,7 +69,15 @@ export default class ProposalField extends SmartField { } _formatValue(value) { - return scout.nvl(value, ''); + if (objects.isNullOrUndefined(value)) { + return ''; + } + + if (this.lookupRow) { + return this._formatLookupRow(this.lookupRow); + } + + return value; } _validateValue(value) { @@ -161,12 +169,11 @@ export default class ProposalField extends SmartField { */ _copyValuesFromField(otherField) { if (this.lookupRow !== otherField.lookupRow) { - this.setLookupRow(otherField.lookupRow); + this._setLookupRow(otherField.lookupRow); // only set property lookup } this.setErrorStatus(otherField.errorStatus); - if (this.value !== otherField.value) { - this.setValue(otherField.value); - } + this.setDisplayText(otherField.displayText); + this.setValue(otherField.value); } _acceptInput(sync, searchText, searchTextEmpty, searchTextChanged, selectedLookupRow) { @@ -176,8 +183,8 @@ export default class ProposalField extends SmartField { return; } - // Do nothing when search text is equals to the text of the current lookup row - if (!selectedLookupRow && this.lookupRow && this.lookupRow.text === searchText) { + // Do nothing when search text did not change and is equals to the text of the current lookup row + if (!searchTextChanged && !selectedLookupRow && this.lookupRow && this.lookupRow.text === searchText) { $.log.isDebugEnabled() && $.log.debug('(ProposalField#_acceptInput) unchanged: text is equals. Close popup'); this._inputAccepted(false); return; diff --git a/eclipse-scout-core/test/form/fields/smartfield/ProposalFieldSpec.js b/eclipse-scout-core/test/form/fields/smartfield/ProposalFieldSpec.js index c1d91a74859..66661db4349 100644 --- a/eclipse-scout-core/test/form/fields/smartfield/ProposalFieldSpec.js +++ b/eclipse-scout-core/test/form/fields/smartfield/ProposalFieldSpec.js @@ -1,14 +1,15 @@ /* - * Copyright (c) 2010-2019 BSI Business Systems Integration AG. + * Copyright (c) 2010-2024 BSI Business Systems Integration AG. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html + * https://www.eclipse.org/legal/epl-v10.html * * Contributors: * BSI Business Systems Integration AG - initial API and implementation */ -import {scout, Status} from '../../../../src/index'; +import {ObjectFactory, scout, Status} from '../../../../src/index'; +import {proposalFieldSpecHelper} from '../../../../src/testing'; describe('ProposalField', () => { @@ -157,4 +158,423 @@ describe('ProposalField', () => { field._acceptInput(false, 'Bar', false, true, null); expect(field.getErrorStatus).toBeUndefined(); }); + + describe('displayText, value, errorStatus and lookupRow are always in a consistent state', () => { + + beforeEach(() => { + jasmine.clock().uninstall(); + ObjectFactory.get().register('SpecProposalField', () => proposalFieldSpecHelper.createSpecProposalField()); + field = scout.create('SpecProposalField', { + parent: session.desktop, + lookupCall: { + objectType: 'StaticLookupCall', + data: [ + ['ok', 'ok'], + ['warning', 'warning'], + ['throw', 'throw'], + ['no error', 'no error'] + ] + } + }); + field.addValidator(value => { + field.clearErrorStatus(); + + if ('ok' === value) { + field.setErrorStatus(Status.ok({message: 'This has severity OK.'})); + return value; + } + + if ('warning' === value) { + field.setErrorStatus(Status.warning({message: 'This has severity WARNING.'})); + return value; + } + + if ('throw' === value) { + throw 'This is an exception.'; + } + + return value; + }); + }); + + afterEach(() => { + ObjectFactory.get().unregister('SpecProposalField'); + }); + + // foo > ok > foo > ok + + it('write \'foo\' > write \'ok\' > write \'foo\' > write \'ok\' (touchMode: false)', async () => + await testProposalFieldInputs( + ['foo', 'ok', 'foo', 'ok'], + false)); + + it('write \'foo\' > write \'ok\' > write \'foo\' > write \'ok\' (touchMode: true)', async () => + await testProposalFieldInputs( + ['foo', 'ok', 'foo', 'ok'], + true)); + + it('write \'foo\' > lookup \'ok\' > write \'foo\' > lookup \'ok\' (touchMode: false)', async () => + await testProposalFieldInputs( + ['foo', {text: 'ok', lookup: true}, 'foo', {text: 'ok', lookup: true}], + false)); + + it('write \'foo\' > lookup \'ok\' > write \'foo\' > lookup \'ok\' (touchMode: true)', async () => + await testProposalFieldInputs( + ['foo', {text: 'ok', lookup: true}, 'foo', {text: 'ok', lookup: true}], + true)); + + // no error > ok > no error > ok + + it('write \'no error\' > lookup \'ok\' > write \'no error\' > lookup \'ok\' (touchMode: false)', async () => + await testProposalFieldInputs( + ['no error', {text: 'ok', lookup: true}, 'no error', {text: 'ok', lookup: true}], + false)); + + it('write \'no error\' > lookup \'ok\' > write \'no error\' > lookup \'ok\' (touchMode: true)', async () => + await testProposalFieldInputs( + ['no error', {text: 'ok', lookup: true}, 'no error', {text: 'ok', lookup: true}], + true)); + + it('lookup \'no error\' > lookup \'ok\' > lookup \'no error\' > lookup \'ok\' (touchMode: false)', async () => + await testProposalFieldInputs( + [{text: 'no error', lookup: true}, {text: 'ok', lookup: true}, {text: 'no error', lookup: true}, {text: 'ok', lookup: true}], + false)); + + it('lookup \'no error\' > lookup \'ok\' > lookup \'no error\' > lookup \'ok\' (touchMode: true)', async () => + await testProposalFieldInputs( + [{text: 'no error', lookup: true}, {text: 'ok', lookup: true}, {text: 'no error', lookup: true}, {text: 'ok', lookup: true}], + true)); + + // foo > throw > foo > throw + + it('write \'foo\' > write \'throw\' > write \'foo\' > write \'throw\' (touchMode: false)', async () => + await testProposalFieldInputs( + ['foo', 'throw', 'foo', 'throw'], + false)); + + it('write \'foo\' > write \'throw\' > write \'foo\' > write \'throw\' (touchMode: true)', async () => + await testProposalFieldInputs( + ['foo', 'throw', 'foo', 'throw'], + true)); + + it('write \'foo\' > lookup \'throw\' > write \'foo\' > lookup \'throw\' (touchMode: false)', async () => + await testProposalFieldInputs( + ['foo', {text: 'throw', lookup: true}, 'foo', {text: 'throw', lookup: true}], + false)); + + it('write \'foo\' > lookup \'throw\' > write \'foo\' > lookup \'throw\' (touchMode: true)', async () => + await testProposalFieldInputs( + ['foo', {text: 'throw', lookup: true}, 'foo', {text: 'throw', lookup: true}], + true)); + + // no error > throw > no error > throw + + it('write \'no error\' > lookup \'throw\' > write \'no error\' > lookup \'throw\' (touchMode: false)', async () => + await testProposalFieldInputs( + ['no error', {text: 'throw', lookup: true}, 'no error', {text: 'throw', lookup: true}], + false)); + + it('write \'no error\' > lookup \'throw\' > write \'no error\' > lookup \'throw\' (touchMode: true)', async () => + await testProposalFieldInputs( + ['no error', {text: 'throw', lookup: true}, 'no error', {text: 'throw', lookup: true}], + true)); + + it('lookup \'no error\' > lookup \'throw\' > lookup \'no error\' > lookup \'throw\' (touchMode: false)', async () => + await testProposalFieldInputs( + [{text: 'no error', lookup: true}, {text: 'throw', lookup: true}, {text: 'no error', lookup: true}, {text: 'throw', lookup: true}], + false)); + + it('lookup \'no error\' > lookup \'throw\' > lookup \'no error\' > lookup \'throw\' (touchMode: true)', async () => + await testProposalFieldInputs( + [{text: 'no error', lookup: true}, {text: 'throw', lookup: true}, {text: 'no error', lookup: true}, {text: 'throw', lookup: true}], + true)); + + // ok > throw > ok > throw + + it('write \'ok\' > write \'throw\' > write \'ok\' > write \'throw\' (touchMode: false)', async () => + await testProposalFieldInputs( + ['ok', 'throw', 'ok', 'throw'], + false)); + + it('write \'ok\' > write \'throw\' > write \'ok\' > write \'throw\' (touchMode: true)', async () => + await testProposalFieldInputs( + ['ok', 'throw', 'ok', 'throw'], + true)); + + it('write \'ok\' > write \'throw\' > write \'ok\' > lookup \'throw\' (touchMode: false)', async () => + await testProposalFieldInputs( + ['ok', 'throw', 'ok', {text: 'throw', lookup: true}], + false)); + + it('write \'ok\' > write \'throw\' > write \'ok\' > lookup \'throw\' (touchMode: true)', async () => + await testProposalFieldInputs( + ['ok', 'throw', 'ok', {text: 'throw', lookup: true}], + true)); + + it('write \'ok\' > write \'throw\' > lookup \'ok\' > write \'throw\' (touchMode: false)', async () => + await testProposalFieldInputs( + ['ok', 'throw', {text: 'ok', lookup: true}, 'throw'], + false)); + + it('write \'ok\' > write \'throw\' > lookup \'ok\' > write \'throw\' (touchMode: true)', async () => + await testProposalFieldInputs( + ['ok', 'throw', {text: 'ok', lookup: true}, 'throw'], + true)); + + it('write \'ok\' > write \'throw\' > lookup \'ok\' > lookup \'throw\' (touchMode: false)', async () => + await testProposalFieldInputs( + ['ok', 'throw', {text: 'ok', lookup: true}, {text: 'throw', lookup: true}], + false)); + + it('write \'ok\' > write \'throw\' > lookup \'ok\' > lookup \'throw\' (touchMode: true)', async () => + await testProposalFieldInputs( + ['ok', 'throw', {text: 'ok', lookup: true}, {text: 'throw', lookup: true}], + true)); + + it('write \'ok\' > lookup \'throw\' > write \'ok\' > write \'throw\' (touchMode: false)', async () => + await testProposalFieldInputs( + ['ok', {text: 'throw', lookup: true}, 'ok', 'throw'], + false)); + + it('write \'ok\' > lookup \'throw\' > write \'ok\' > write \'throw\' (touchMode: true)', async () => + await testProposalFieldInputs( + ['ok', {text: 'throw', lookup: true}, 'ok', 'throw'], + true)); + + it('write \'ok\' > lookup \'throw\' > write \'ok\' > lookup \'throw\' (touchMode: false)', async () => + await testProposalFieldInputs( + ['ok', {text: 'throw', lookup: true}, 'ok', {text: 'throw', lookup: true}], + false)); + + it('write \'ok\' > lookup \'throw\' > write \'ok\' > lookup \'throw\' (touchMode: true)', async () => + await testProposalFieldInputs( + ['ok', {text: 'throw', lookup: true}, 'ok', {text: 'throw', lookup: true}], + true)); + + it('write \'ok\' > lookup \'throw\' > lookup \'ok\' > write \'throw\' (touchMode: false)', async () => + await testProposalFieldInputs( + ['ok', {text: 'throw', lookup: true}, {text: 'ok', lookup: true}, 'throw'], + false)); + + it('write \'ok\' > lookup \'throw\' > lookup \'ok\' > write \'throw\' (touchMode: true)', async () => + await testProposalFieldInputs( + ['ok', {text: 'throw', lookup: true}, {text: 'ok', lookup: true}, 'throw'], + true)); + + it('write \'ok\' > lookup \'throw\' > lookup \'ok\' > lookup \'throw\' (touchMode: false)', async () => + await testProposalFieldInputs( + ['ok', {text: 'throw', lookup: true}, {text: 'ok', lookup: true}, {text: 'throw', lookup: true}], + false)); + + it('write \'ok\' > lookup \'throw\' > lookup \'ok\' > lookup \'throw\' (touchMode: true)', async () => + await testProposalFieldInputs( + ['ok', {text: 'throw', lookup: true}, {text: 'ok', lookup: true}, {text: 'throw', lookup: true}], + true)); + + it('lookup \'ok\' > write \'throw\' > write \'ok\' > write \'throw\' (touchMode: false)', async () => + await testProposalFieldInputs( + [{text: 'ok', lookup: true}, 'throw', 'ok', 'throw'], + false)); + + it('lookup \'ok\' > write \'throw\' > write \'ok\' > write \'throw\' (touchMode: true)', async () => + await testProposalFieldInputs( + [{text: 'ok', lookup: true}, 'throw', 'ok', 'throw'], + true)); + + it('lookup \'ok\' > write \'throw\' > write \'ok\' > lookup \'throw\' (touchMode: false)', async () => + await testProposalFieldInputs( + [{text: 'ok', lookup: true}, 'throw', 'ok', {text: 'throw', lookup: true}], + false)); + + it('lookup \'ok\' > write \'throw\' > write \'ok\' > lookup \'throw\' (touchMode: true)', async () => + await testProposalFieldInputs( + [{text: 'ok', lookup: true}, 'throw', 'ok', {text: 'throw', lookup: true}], + true)); + + it('lookup \'ok\' > write \'throw\' > lookup \'ok\' > write \'throw\' (touchMode: false)', async () => + await testProposalFieldInputs( + [{text: 'ok', lookup: true}, 'throw', {text: 'ok', lookup: true}, 'throw'], + false)); + + it('lookup \'ok\' > write \'throw\' > lookup \'ok\' > write \'throw\' (touchMode: true)', async () => + await testProposalFieldInputs( + [{text: 'ok', lookup: true}, 'throw', {text: 'ok', lookup: true}, 'throw'], + true)); + + it('lookup \'ok\' > write \'throw\' > lookup \'ok\' > lookup \'throw\' (touchMode: false)', async () => + await testProposalFieldInputs( + [{text: 'ok', lookup: true}, 'throw', {text: 'ok', lookup: true}, {text: 'throw', lookup: true}], + false)); + + it('lookup \'ok\' > write \'throw\' > lookup \'ok\' > lookup \'throw\' (touchMode: true)', async () => + await testProposalFieldInputs( + [{text: 'ok', lookup: true}, 'throw', {text: 'ok', lookup: true}, {text: 'throw', lookup: true}], + true)); + + it('lookup \'ok\' > lookup \'throw\' > write \'ok\' > write \'throw\' (touchMode: false)', async () => + await testProposalFieldInputs( + [{text: 'ok', lookup: true}, {text: 'throw', lookup: true}, 'ok', 'throw'], + false)); + + it('lookup \'ok\' > lookup \'throw\' > write \'ok\' > write \'throw\' (touchMode: true)', async () => + await testProposalFieldInputs( + [{text: 'ok', lookup: true}, {text: 'throw', lookup: true}, 'ok', 'throw'], + true)); + + it('lookup \'ok\' > lookup \'throw\' > write \'ok\' > lookup \'throw\' (touchMode: false)', async () => + await testProposalFieldInputs( + [{text: 'ok', lookup: true}, {text: 'throw', lookup: true}, 'ok', {text: 'throw', lookup: true}], + false)); + + it('lookup \'ok\' > lookup \'throw\' > write \'ok\' > lookup \'throw\' (touchMode: true)', async () => + await testProposalFieldInputs( + [{text: 'ok', lookup: true}, {text: 'throw', lookup: true}, 'ok', {text: 'throw', lookup: true}], + true)); + + it('lookup \'ok\' > lookup \'throw\' > lookup \'ok\' > write \'throw\' (touchMode: false)', async () => + await testProposalFieldInputs( + [{text: 'ok', lookup: true}, {text: 'throw', lookup: true}, {text: 'ok', lookup: true}, 'throw'], + false)); + + it('lookup \'ok\' > lookup \'throw\' > lookup \'ok\' > write \'throw\' (touchMode: true)', async () => + await testProposalFieldInputs( + [{text: 'ok', lookup: true}, {text: 'throw', lookup: true}, {text: 'ok', lookup: true}, 'throw'], + true)); + + it('lookup \'ok\' > lookup \'throw\' > lookup \'ok\' > lookup \'throw\' (touchMode: false)', async () => + await testProposalFieldInputs( + [{text: 'ok', lookup: true}, {text: 'throw', lookup: true}, {text: 'ok', lookup: true}, {text: 'throw', lookup: true}], + false)); + + it('lookup \'ok\' > lookup \'throw\' > lookup \'ok\' > lookup \'throw\' (touchMode: true)', async () => + await testProposalFieldInputs( + [{text: 'ok', lookup: true}, {text: 'throw', lookup: true}, {text: 'ok', lookup: true}, {text: 'throw', lookup: true}], + true)); + + // ok > throw > warning + + it('write \'ok\' > write \'throw\' > write \'warning\' (touchMode: false)', async () => + await testProposalFieldInputs( + ['ok', 'throw', 'warning'], + false)); + + it('write \'ok\' > write \'throw\' > write \'warning\' (touchMode: true)', async () => + await testProposalFieldInputs( + ['ok', 'throw', 'warning'], + true)); + + it('write \'ok\' > write \'throw\' > lookup \'warning\' (touchMode: false)', async () => + await testProposalFieldInputs( + ['ok', 'throw', {text: 'warning', lookup: true}], + false)); + + it('write \'ok\' > write \'throw\' > lookup \'warning\' (touchMode: true)', async () => + await testProposalFieldInputs( + ['ok', 'throw', {text: 'warning', lookup: true}], + true)); + + it('write \'ok\' > lookup \'throw\' > write \'warning\' (touchMode: false)', async () => + await testProposalFieldInputs( + ['ok', {text: 'throw', lookup: true}, 'warning'], + false)); + + it('write \'ok\' > lookup \'throw\' > write \'warning\' (touchMode: true)', async () => + await testProposalFieldInputs( + ['ok', {text: 'throw', lookup: true}, 'warning'], + true)); + + it('write \'ok\' > lookup \'throw\' > lookup \'warning\' (touchMode: false)', async () => + await testProposalFieldInputs( + ['ok', {text: 'throw', lookup: true}, {text: 'warning', lookup: true}], + false)); + + it('write \'ok\' > lookup \'throw\' > lookup \'warning\' (touchMode: true)', async () => + await testProposalFieldInputs( + ['ok', {text: 'throw', lookup: true}, {text: 'warning', lookup: true}], + true)); + + it('lookup \'ok\' > write \'throw\' > write \'warning\' (touchMode: false)', async () => + await testProposalFieldInputs( + [{text: 'ok', lookup: true}, 'throw', 'warning'], + false)); + + it('lookup \'ok\' > write \'throw\' > write \'warning\' (touchMode: true)', async () => + await testProposalFieldInputs( + [{text: 'ok', lookup: true}, 'throw', 'warning'], + true)); + + it('lookup \'ok\' > write \'throw\' > lookup \'warning\' (touchMode: false)', async () => + await testProposalFieldInputs( + [{text: 'ok', lookup: true}, 'throw', {text: 'warning', lookup: true}], + false)); + + it('lookup \'ok\' > write \'throw\' > lookup \'warning\' (touchMode: true)', async () => + await testProposalFieldInputs( + [{text: 'ok', lookup: true}, 'throw', {text: 'warning', lookup: true}], + true)); + + it('lookup \'ok\' > lookup \'throw\' > write \'warning\' (touchMode: false)', async () => + await testProposalFieldInputs( + [{text: 'ok', lookup: true}, {text: 'throw', lookup: true}, 'warning'], + false)); + + it('lookup \'ok\' > lookup \'throw\' > write \'warning\' (touchMode: true)', async () => + await testProposalFieldInputs( + [{text: 'ok', lookup: true}, {text: 'throw', lookup: true}, 'warning'], + true)); + + it('lookup \'ok\' > lookup \'throw\' > lookup \'warning\' (touchMode: false)', async () => + await testProposalFieldInputs( + [{text: 'ok', lookup: true}, {text: 'throw', lookup: true}, {text: 'warning', lookup: true}], + false)); + + it('lookup \'ok\' > lookup \'throw\' > lookup \'warning\' (touchMode: true)', async () => + await testProposalFieldInputs( + [{text: 'ok', lookup: true}, {text: 'throw', lookup: true}, {text: 'warning', lookup: true}], + true)); + + async function testProposalFieldInputs(inputs, touchMode) { + await proposalFieldSpecHelper.testProposalFieldInputs(field, inputs, touchMode, { + afterInput: (input) => { + const {text, lookup} = input; + + // displayText always equals text + expect(field.displayText).toBe(text); + + // value equals text iff there is no validation error (see validator) + if ('throw' === text) { + expect(field.value).not.toBe(text); + } else { + expect(field.value).toBe(text); + } + + // correct errorStatus is set (see validator) + if ('ok' === text) { + expect(field.errorStatus).not.toBeNull(); + expect(field.errorStatus.severity).toBe(Status.Severity.OK); + expect(field.errorStatus.message).toBe('This has severity OK.'); + } else if ('warning' === text) { + expect(field.errorStatus).not.toBeNull(); + expect(field.errorStatus.severity).toBe(Status.Severity.WARNING); + expect(field.errorStatus.message).toBe('This has severity WARNING.'); + } else if ('throw' === text) { + expect(field.errorStatus).not.toBeNull(); + expect(field.errorStatus.severity).toBe(Status.Severity.ERROR); + expect(field.errorStatus.message).toBe('This is an exception.'); + } else { + expect(field.errorStatus).toBeNull(); + } + + // lookupRow is set and contains the correct values iff a lookupRow was selected + if (lookup) { + expect(field.lookupRow).not.toBeNull(); + expect(field.lookupRow.text).toBe(text); + expect(field.lookupRow.key).toBe(text); + } else { + expect(field.lookupRow).toBeNull(); + } + } + }); + } + }); });