You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by ah...@apache.org on 2020/07/24 07:34:49 UTC

[isis] branch master updated: ISIS-2340: collect parameter bindables into their own ParameterModel

This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/isis.git


The following commit(s) were added to refs/heads/master by this push:
     new fa6edee  ISIS-2340: collect parameter bindables into their own ParameterModel
fa6edee is described below

commit fa6edee7ff4ade24f2479517a3afae1d2200f94b
Author: Andi Huber <ah...@apache.org>
AuthorDate: Fri Jul 24 09:34:27 2020 +0200

    ISIS-2340: collect parameter bindables into their own ParameterModel
---
 .../core/commons/internal/primitives/_Ints.java    |  59 +++++-
 .../core/commons/internal/primitives/IntsTest.java |  27 +++
 .../interactions/managed/ActionInteraction.java    |   3 +-
 .../managed/ParameterNegotiationModel.java         | 202 ++++++++++++++-------
 .../interaction/InteractionDemo_negotiate.java     |   4 +
 .../testdomain/interact/HasActionValidation.java   |   4 +-
 .../interact/ScalarParamNegotiationTest.java       |  30 ++-
 .../interact/SimulatedUiAutoComplete.java          |   3 +
 .../testdomain/interact/SimulatedUiSubmit.java     |   2 +-
 9 files changed, 255 insertions(+), 79 deletions(-)

diff --git a/core/commons/src/main/java/org/apache/isis/core/commons/internal/primitives/_Ints.java b/core/commons/src/main/java/org/apache/isis/core/commons/internal/primitives/_Ints.java
index 7fc86e7..7de48aa 100644
--- a/core/commons/src/main/java/org/apache/isis/core/commons/internal/primitives/_Ints.java
+++ b/core/commons/src/main/java/org/apache/isis/core/commons/internal/primitives/_Ints.java
@@ -20,6 +20,7 @@ package org.apache.isis.core.commons.internal.primitives;
 
 import java.util.Optional;
 import java.util.OptionalInt;
+import java.util.PrimitiveIterator;
 import java.util.function.Consumer;
 import java.util.stream.IntStream;
 
@@ -29,7 +30,9 @@ import org.apache.isis.core.commons.internal.exceptions._Exceptions;
 
 import static org.apache.isis.core.commons.internal.base._With.requires;
 
+import lombok.AccessLevel;
 import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
 import lombok.Value;
 import lombok.val;
 import lombok.experimental.UtilityClass;
@@ -59,11 +62,24 @@ public class _Ints {
         public static @NonNull Bound exclusive(int value) { return of(value, true); }
     }
     
-    @Value(staticConstructor = "of")
+    @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
     public static class Range {
-        @NonNull Bound lowerBound;
-        @NonNull Bound upperBound;
+        
+        public static Range empty() {
+            return new Range(null, null, true);
+        }
+        
+        public static Range of(
+                @NonNull Bound lowerBound,
+                @NonNull Bound upperBound) {
+            return new Range(lowerBound, upperBound, false);
+        }
+        
+        private final Bound lowerBound;
+        private final Bound upperBound;
+        private final boolean empty;
         public boolean contains(int value) {
+            if(empty) return false;
             val isBelowLower = lowerBound.isInclusive() 
                     ? value < lowerBound.getValue() 
                     : value <= lowerBound.getValue();  
@@ -83,6 +99,7 @@ public class _Ints {
          * @return the value or if not within range, the nearest integer to the value, that is within range   
          */
         public int bounded(int value) {
+            if(empty) return value; // noop
             if(contains(value)) {
                 return value;
             }
@@ -95,12 +112,15 @@ public class _Ints {
                     : nearestToUpper;
         }
         private int nearestToLower() {
+            if(empty) throw _Exceptions.unsupportedOperation();
             return lowerBound.isInclusive() ? lowerBound.getValue() : lowerBound.getValue()+1;  
         }
         private int nearestToUpper() {
+            if(empty) throw _Exceptions.unsupportedOperation();
             return upperBound.isInclusive() ? upperBound.getValue() : upperBound.getValue()-1;
         }
         public @NonNull Optional<Range> intersect(@NonNull Range other) {
+            if(empty) return Optional.empty();
             final int s1 = this.nearestToLower();
             final int e1 = this.nearestToUpper();
             final int s2 = other.nearestToLower();
@@ -114,17 +134,44 @@ public class _Ints {
         }
         @Override
         public String toString() {
+            if(empty) return "[]";
             return String.format("%s%d,%d%S", 
                     lowerBound.isInclusive() ? '[' : '(', lowerBound.getValue(),
                     upperBound.getValue(), upperBound.isInclusive() ? ']' : ')');
         }
         public IntStream stream() {
+            if(empty) return IntStream.empty();
             return IntStream.rangeClosed(nearestToLower(), nearestToUpper());
         }
+        public PrimitiveIterator.OfInt iterator() {
+            if(empty) return IntStream.empty().iterator();
+            return new PrimitiveIterator.OfInt() {
+                int next = nearestToLower();
+                final int upperIncluded = nearestToUpper();
+
+                @Override
+                public int nextInt() {
+                    if(!hasNext()) {
+                        throw _Exceptions.noSuchElement();
+                    }
+                    int result = next;
+                    next++;
+                    return result;
+                }
+
+                @Override
+                public boolean hasNext() {
+                    return next <= upperIncluded;
+                }
+            };
+        }
     }
     
     // -- RANGE FACTORIES
     
+    /**
+     * Range includes a and b.
+     */
     public static Range rangeClosed(int a, int b) {
         if(a>b) {
             throw _Exceptions.illegalArgument("bounds must be ordered in [%d, %d]", a, b);
@@ -132,7 +179,13 @@ public class _Ints {
         return Range.of(Bound.inclusive(a), Bound.inclusive(b));
     }
     
+    /**
+     * Range includes a but not b.
+     */
     public static Range rangeOpenEnded(int a, int b) {
+        if(a==b) {
+            return Range.empty();
+        }
         if(a>=b) {
             throw _Exceptions.illegalArgument("bounds must be ordered in [%d, %d]", a, b);
         }
diff --git a/core/commons/src/test/java/org/apache/isis/core/commons/internal/primitives/IntsTest.java b/core/commons/src/test/java/org/apache/isis/core/commons/internal/primitives/IntsTest.java
index 680aa6f..f3c75a6 100644
--- a/core/commons/src/test/java/org/apache/isis/core/commons/internal/primitives/IntsTest.java
+++ b/core/commons/src/test/java/org/apache/isis/core/commons/internal/primitives/IntsTest.java
@@ -38,6 +38,12 @@ class IntsTest {
     }
 
     @Test
+    void emptyRange() {
+        val range = _Ints.rangeOpenEnded(3, 3);
+        assertEquals("[]", range.toString());
+    }
+    
+    @Test
     void rangeIntersection() {
         
         val intersection = _Ints.rangeClosed(3, 7)
@@ -75,5 +81,26 @@ class IntsTest {
         
         assertEquals("[5,7]", intersection.toString());
     }
+    
+    @Test
+    void rangeIterator() {
+
+        val sb = new StringBuilder();
+        
+        val rangeIterator1 = _Ints.rangeClosed(3, 7).iterator();
+        while(rangeIterator1.hasNext()) {
+            sb.append(rangeIterator1.nextInt()).append(',');
+        }
+        assertEquals("3,4,5,6,7,", sb.toString());
+        sb.setLength(0);
+        
+        val rangeIterator2 = _Ints.rangeOpenEnded(3, 3).iterator();
+        while(rangeIterator2.hasNext()) {
+            sb.append(rangeIterator2.nextInt()).append(',');
+        }
+        assertEquals("", sb.toString());
+        sb.setLength(0);
+        
+    }
 
 }
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/ActionInteraction.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/ActionInteraction.java
index 90ac5f6..aa7edc2 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/ActionInteraction.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/ActionInteraction.java
@@ -110,8 +110,7 @@ public final class ActionInteraction extends MemberInteraction<ManagedAction, Ac
     }
     
     public ActionInteraction submit(ParameterNegotiationModel pendingArgs) {
-        // TODO validate pendingArgs
-        // TODO only if all is sound, invoke the action
+        pendingArgs.submit(this);
         return this;
     }
     
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/ParameterNegotiationModel.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/ParameterNegotiationModel.java
index 885f450..f3050c1 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/ParameterNegotiationModel.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/ParameterNegotiationModel.java
@@ -18,20 +18,22 @@
  */
 package org.apache.isis.core.metamodel.interactions.managed;
 
+import java.util.stream.IntStream;
+
 import javax.annotation.Nullable;
 
 import org.apache.isis.core.commons.binding.Bindable;
 import org.apache.isis.core.commons.binding.Observable;
 import org.apache.isis.core.commons.collections.Can;
+import org.apache.isis.core.commons.internal.base._Lazy;
+import org.apache.isis.core.commons.internal.binding._BindableAbstract;
 import org.apache.isis.core.commons.internal.binding._Bindables;
 import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy;
 import org.apache.isis.core.metamodel.spec.ManagedObject;
 import org.apache.isis.core.metamodel.spec.feature.ObjectActionParameter;
 
-import lombok.AccessLevel;
 import lombok.Getter;
 import lombok.NonNull;
-import lombok.RequiredArgsConstructor;
 import lombok.val;
 
 
@@ -43,75 +45,82 @@ import lombok.val;
  *  
  * @since 2.0.0
  */
-@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
 public class ParameterNegotiationModel {
     
     public static ParameterNegotiationModel of(
             @NonNull final ActionInteractionHead head,
             @NonNull final Can<ManagedObject> initialParamValues) {
-        return new ParameterNegotiationModel(
-                head, 
-                initialParamValues.map(_Bindables::forValue));
+        return new ParameterNegotiationModel(head, initialParamValues);
+                
     }
-    
+
     @Getter private final ActionInteractionHead head;
-    private final Can<Bindable<ManagedObject>> paramValues;
+    private final Can<ParameterModel> paramModels;
+    private final _BindableAbstract<Boolean> validationFeedbackActive;
+    
+    private ParameterNegotiationModel(
+            @NonNull final ActionInteractionHead head,
+            @NonNull final Can<ManagedObject> initialParamValues) {
+        this.head = head;
+        val paramNrIterator = IntStream.range(0, initialParamValues.size()).iterator();
+        this.paramModels = initialParamValues
+                .map(initialValue->new ParameterModel(paramNrIterator.nextInt(), this, initialValue));
+        this.validationFeedbackActive = _Bindables.forValue(false);
+    }
+    
+    // -- ACTION SPECIFIC
 
+    @NonNull public Can<ManagedObject> getParamValues() {
+        return paramModels.map(ParameterModel::getValue);
+    }
+    
     @NonNull public ManagedObject getActionTarget() {
         return getHead().getTarget();
     }
     
-    @NonNull public ObjectActionParameter getParamMetamodel(int paramNr) {
-        return head.getMetaModel().getParameters().getElseFail(paramNr);
+    @NonNull public Observable<String> getObservableActionValidation() {
+        // TODO listen to any user initiated submit attempt then validate the action
+        // this also turns on validationFeedback
+        return _Bindables.forValue(null);
     }
     
-    @NonNull public Can<ManagedObject> getParamValues() {
-        return paramValues.map(Bindable::getValue);
+    /**
+     * Whether validation feedback is activated. Activates once user attempts to 'submit' an action.
+     */
+    @NonNull public Observable<Boolean> getObservableValidationFeedbackActive() {
+        return validationFeedbackActive;
+    }
+
+    // -- PARAMETER SPECIFIC
+    
+    @NonNull public ObjectActionParameter getParamMetamodel(int paramNr) {
+        return paramModels.getElseFail(paramNr).getMetaModel();
     }
     
     @NonNull public Bindable<ManagedObject> getBindableParamValue(int paramNr) {
-        return paramValues.getElseFail(paramNr);
+        return paramModels.getElseFail(paramNr).getBindableParamValue();
     }
     
     @NonNull public Observable<Can<ManagedObject>> getObservableParamChoices(int paramNr) {
-        val choices = getParamMetamodel(paramNr).getChoices(this, InteractionInitiatedBy.USER);
-        //TODO well not quite, also need to listen to any param value changes, that invalidate these choices
-        return _Bindables.forValue(choices);
+        return paramModels.getElseFail(paramNr).getObservableParamChoices();
     }
     
     @NonNull public Observable<String> getObservableParamValidation(int paramNr) {
-        // TODO listen to any user initiated value changes, then validate the corresponding parameter
-        // but only after validationFeedback has been turned on
-        return _Bindables.forValue(null);
+        return paramModels.getElseFail(paramNr).getObservableParamValidation();
     }
     
     @NonNull public Bindable<String> getBindableParamSearchArgument(int paramNr) {
-        // TODO any changes should trigger an update to observableParamChoices
-        return _Bindables.forValue(null);
-    }
-    
-    @NonNull public Observable<String> getObservableActionValidation() {
-        // TODO listen to any user initiated submit attempt then validate the action
-        // this also turns on validationFeedback
-        return _Bindables.forValue(null);
-    }
-    
-    /**
-     * Whether validation feedback is activated or not. Activates once user attempts to 'submit' an action.
-     */
-    @NonNull public Observable<Boolean> getObservableValidationFeedbackState() {
-        // TODO listen to any user initiated submit attempt then activate validationFeedback
-        return _Bindables.forValue(false);
+        return paramModels.getElseFail(paramNr).getBindableParamSearchArgument();
     }
 
     // -- RATHER INTERNAL ... 
     
     @NonNull public ManagedObject getParamValue(int paramNr) {
-        return paramValues.getElseFail(paramNr).getValue();
+        return paramModels.getElseFail(paramNr).getValue();
     }
 
     public void setParamValue(int paramNr, @NonNull ManagedObject newParamValue) {
-        paramValues.getElseFail(paramNr).setValue(newParamValue);
+        paramModels.getElseFail(paramNr).getBindableParamValue().setValue(newParamValue);
     }
 
     @NonNull public ManagedObject adaptParamValuePojo(int paramNr, @Nullable Object newParamValuePojo) {
@@ -123,31 +132,100 @@ public class ParameterNegotiationModel {
         return paramValue;
     }
 
-    // -- TODO UNDER CONSTRUCTION ...
-    
-//    @RequiredArgsConstructor(staticName = "of")
-//    public static class BindableParameter extends _BindableAbstract<ManagedObject> {
-//
-//        @Getter private final int paramNr;
-//        @Getter @NonNull private final ParameterNegotiationModel model;
-//
-//        public String getName() {
-//            val paramMeta = model.getParamMetamodel(paramNr);
-//            return paramMeta.getName();
-//        }
-//        
-//        @Override
-//        public ManagedObject getValue() {
-//            return model.getParamValue(paramNr);
-//        }
-//        
-//        @Override
-//        public void setValue(final ManagedObject newValue) {
-//            val oldValue = getValue(); 
-//            model.setParamValue(paramNr, newValue);
-//        }
-//        
-//    }
+    public void submit(@NonNull ActionInteraction actionInteraction) {
+        validationFeedbackActive.setValue(true);
+        // TODO validate pendingArgs
+        // TODO only if all is sound, invoke the action
+    }
+    
+    private void onNewParamValue() {
+        paramModels.forEach(ParameterModel::invalidateChoicesAndValidation);
+    }
+    
+    // -- INTERNAL HOLDER OF PARAMETER BINDABLES
+    
+    private static class ParameterModel {
+
+        @Getter private final int paramNr;
+        @Getter @NonNull private final ObjectActionParameter metaModel;
+        @Getter @NonNull private final ParameterNegotiationModel model;
+        @Getter @NonNull private final _BindableAbstract<ManagedObject> bindableParamValue;
+        @Getter @NonNull private final _BindableAbstract<String> observableParamValidation;
+        @Getter @NonNull private final _BindableAbstract<String> bindableParamSearchArgument;
+        @Getter @NonNull private final _BindableAbstract<Can<ManagedObject>> observableParamChoices;
+        
+        private final _Lazy<Can<ManagedObject>> paramChoices = _Lazy.threadSafe(()->
+            getMetaModel().getChoices(getModel(), InteractionInitiatedBy.USER));
+        
+        private final _Lazy<Can<ManagedObject>> autoCompleteChoices = _Lazy.threadSafe(()->
+            getMetaModel().getAutoComplete(getModel(), getBindableParamSearchArgument().getValue(), InteractionInitiatedBy.USER));
+        
+        private final _Lazy<String> validationMessage = _Lazy.threadSafe(()->
+            getMetaModel().isValid(getModel().head, getValue(), InteractionInitiatedBy.USER)); // TODO consider all params
+        
+        private ParameterModel(
+                int paramNr, 
+                @NonNull ParameterNegotiationModel model,
+                @NonNull ManagedObject initialValue) {
+            
+            this.paramNr = paramNr;
+            this.metaModel = model.getHead().getMetaModel().getParameters().getElseFail(paramNr);
+            this.model = model;
+            
+            bindableParamValue = _Bindables.forValue(initialValue);
+            bindableParamValue.addListener((e,o,n)->{
+                getModel().onNewParamValue();
+            });
+            
+            
+            bindableParamSearchArgument = _Bindables.forValue(null);
+            bindableParamSearchArgument.addListener((e,o,n)->{
+                autoCompleteChoices.clear();
+                //TODO also trigger updates to be propagated to UI
+            });
+
+            // has either autoComplete, choices, or none
+            observableParamChoices = metaModel.hasAutoComplete()
+            ? new _BindableAbstract<Can<ManagedObject>>() {
+                @Override
+                public Can<ManagedObject> getValue() {
+                    return autoCompleteChoices.get();
+                }
+            }
+            : metaModel.hasChoices() 
+                ? new _BindableAbstract<Can<ManagedObject>>() {
+                    @Override
+                    public Can<ManagedObject> getValue() {
+                        return paramChoices.get();
+                    }
+                }
+                : _Bindables.forValue(Can.empty());
+            
+            // TODO listen to any user initiated value changes, then validate the corresponding parameter
+            // but only after validationFeedback has been turned on
+            observableParamValidation = _Bindables.forValue(null);
+            
+        }
+        
+        public String getName() {
+            return getMetaModel().getName();
+        }
+        
+        public @NonNull ManagedObject getValue() {
+            return bindableParamValue.getValue();
+        }
+        
+        public void invalidateChoicesAndValidation() {
+            paramChoices.clear();
+            autoCompleteChoices.clear();
+            validationMessage.clear();
+            //TODO also trigger updates to be propagated to UI
+        }
+        
+    }
+
+
+
 
     
     
diff --git a/examples/smoketests/stable/src/main/java/org/apache/isis/testdomain/model/interaction/InteractionDemo_negotiate.java b/examples/smoketests/stable/src/main/java/org/apache/isis/testdomain/model/interaction/InteractionDemo_negotiate.java
index a92d61c..4eea0b4 100644
--- a/examples/smoketests/stable/src/main/java/org/apache/isis/testdomain/model/interaction/InteractionDemo_negotiate.java
+++ b/examples/smoketests/stable/src/main/java/org/apache/isis/testdomain/model/interaction/InteractionDemo_negotiate.java
@@ -21,6 +21,7 @@ package org.apache.isis.testdomain.model.interaction;
 import java.util.stream.IntStream;
 
 import org.apache.isis.applib.annotation.Action;
+import org.apache.isis.core.commons.internal.base._Strings;
 import org.apache.isis.extensions.modelannotation.applib.annotation.Model;
 import org.apache.isis.testdomain.model.interaction.InteractionDemo_negotiate.Params.NumberRange;
 
@@ -121,6 +122,9 @@ public class InteractionDemo_negotiate {
     }
     
     private int[] searchWithin(NumberRange range, String search) {
+        if(_Strings.isEmpty(search)) {
+            return new int[0];
+        }
         return IntStream.of(range.numbers())
         .filter(e->(""+e).contains(search))
         .toArray();
diff --git a/examples/smoketests/stable/src/test/java/org/apache/isis/testdomain/interact/HasActionValidation.java b/examples/smoketests/stable/src/test/java/org/apache/isis/testdomain/interact/HasActionValidation.java
index 351f645..406c408 100644
--- a/examples/smoketests/stable/src/test/java/org/apache/isis/testdomain/interact/HasActionValidation.java
+++ b/examples/smoketests/stable/src/test/java/org/apache/isis/testdomain/interact/HasActionValidation.java
@@ -26,8 +26,8 @@ abstract class HasActionValidation {
 
     protected final Bindable<String> validationMessage = _Bindables.empty();
     
-    public void bind(ParameterNegotiationModel pendingArgs, int paramNr) {
-        validationMessage.bind(pendingArgs.getObservableParamValidation(paramNr));
+    public void bind(ParameterNegotiationModel pendingArgs) {
+        validationMessage.bind(pendingArgs.getObservableActionValidation());
     }
 
     public String getValidationMessage() {
diff --git a/examples/smoketests/stable/src/test/java/org/apache/isis/testdomain/interact/ScalarParamNegotiationTest.java b/examples/smoketests/stable/src/test/java/org/apache/isis/testdomain/interact/ScalarParamNegotiationTest.java
index cdad08c..6384a59 100644
--- a/examples/smoketests/stable/src/test/java/org/apache/isis/testdomain/interact/ScalarParamNegotiationTest.java
+++ b/examples/smoketests/stable/src/test/java/org/apache/isis/testdomain/interact/ScalarParamNegotiationTest.java
@@ -46,11 +46,8 @@ import lombok.val;
                 "isis.core.meta-model.introspector.mode=FULL",
                 "isis.applib.annotation.domain-object.editing=TRUE",
                 "isis.core.meta-model.validator.explicit-object-type=FALSE", // does not override any of the imports
-                "logging.level.DependentArgUtils=DEBUG"
         })
 @TestPropertySource({
-    //IsisPresets.DebugMetaModel,
-    //IsisPresets.DebugProgrammingModel,
     IsisPresets.SilenceMetaModel,
     IsisPresets.SilenceProgrammingModel
 })
@@ -78,7 +75,7 @@ class ScalarParamNegotiationTest extends InteractionTestAbstract {
     SimulatedUiSubmit uiSubmit;
     
     @BeforeEach
-    void setUp() {
+    void setUpSimulatedUi() {
 
         val actionInteraction = startActionInteractionOn(InteractionDemo.class, "negotiate")
                 .checkVisibility(Where.OBJECT_FORMS)
@@ -140,22 +137,37 @@ class ScalarParamNegotiationTest extends InteractionTestAbstract {
         
         assertEmpty(uiSubmit.getValidationMessage());
         
+        // verify that validation feedback is not active
+        
+        assertFalse(pendingArgs.getObservableValidationFeedbackActive().getValue());
+        
     }
     
     @Test
     void paramC_whenSettingSearchArgument_shouldProvideChoices() {
-        //TODO implement
+        // TODO verify that the following also triggers change listeners
+        uiParamC.setSimulatedSearchArgument("-"); // select for all negative and odd numbers
+        assertComponentWiseUnwrappedEquals(new int[] {-3,-1}, uiParamC.getChoices());
+        // TODO such a change might set or clear paramC validation message once validation feedback is active
     }
 
     @Test
     void paramRangeA_whenChanging_shouldUpdateParamAChoices() {
-        //TODO implement
+        // TODO verify that the following also triggers change listeners
+        uiParamRangeA.simulateChoiceSelect(NumberRange.NEGATIVE.ordinal());
+        assertEquals(NumberRange.NEGATIVE, uiParamRangeA.getValue().getPojo());
+        assertComponentWiseUnwrappedEquals(NumberRange.NEGATIVE.numbers(), uiParamA.getChoices());
+        // TODO such a change might set or clear paramA validation message once validation feedback is active
     }
     
     @Test
-    void whenSimulatedSubmit_shouldTurnOnValidation() {
-        //TODO simulated submit attempt
-        //TODO should turn on validation feedback
+    void whenSimulatedSubmit_shouldActivateValidationFeedback() {
+        // simulated submit attempt, should activate validation feedback
+        uiSubmit.simulateSubmit();
+        assertTrue(pendingArgs.getObservableValidationFeedbackActive().getValue());
+        //TODO unless all validations give green light, submission must be vetoed
+        //TODO exceptions that occur during action invocation could either be rendered 
+        //     as message, error page or action validation message 
     }
   
 
diff --git a/examples/smoketests/stable/src/test/java/org/apache/isis/testdomain/interact/SimulatedUiAutoComplete.java b/examples/smoketests/stable/src/test/java/org/apache/isis/testdomain/interact/SimulatedUiAutoComplete.java
index 3cb9df2..aa5ef43 100644
--- a/examples/smoketests/stable/src/test/java/org/apache/isis/testdomain/interact/SimulatedUiAutoComplete.java
+++ b/examples/smoketests/stable/src/test/java/org/apache/isis/testdomain/interact/SimulatedUiAutoComplete.java
@@ -32,5 +32,8 @@ public class SimulatedUiAutoComplete extends SimulatedUiChoices {
         super.bind(pendingArgs, paramNr);
     }
 
+    public void setSimulatedSearchArgument(String search) {
+        searchText.setValue(search);
+    }
 
 }
diff --git a/examples/smoketests/stable/src/test/java/org/apache/isis/testdomain/interact/SimulatedUiSubmit.java b/examples/smoketests/stable/src/test/java/org/apache/isis/testdomain/interact/SimulatedUiSubmit.java
index 667ec09..1abf4c0 100644
--- a/examples/smoketests/stable/src/test/java/org/apache/isis/testdomain/interact/SimulatedUiSubmit.java
+++ b/examples/smoketests/stable/src/test/java/org/apache/isis/testdomain/interact/SimulatedUiSubmit.java
@@ -26,7 +26,7 @@ public class SimulatedUiSubmit extends HasActionValidation {
     private Runnable doSubmit; // might require a weak reference when actually implementing
     
     public void bind(final ActionInteraction interaction, final ParameterNegotiationModel pendingArgs) {
-        validationMessage.bind(pendingArgs.getObservableActionValidation());
+        super.bind(pendingArgs);
         doSubmit = ()->interaction.submit(pendingArgs);
     }