Repository: wix/Detox
Pull request #2438 (2020-10-28 19:27:20)

Introduce strict-mode to taps
- [ ] This is a small change 
- [ ] This change has been discussed in issue #<?> and the solution has been agreed upon with maintainers.

---

**Description:**
Allow for passing a `{ strict: false }` argument to the various element tapping API's, in order to hold back error checking. Currently, this includes only the tap turning into a long-tap, validation.


Patch:
diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/DetoxAction.java b/detox/android/detox/src/main/java/com/wix/detox/espresso/DetoxAction.java
index 108efcb7d..5c9b55ad4 100644
--- a/detox/android/detox/src/main/java/com/wix/detox/espresso/DetoxAction.java
+++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/DetoxAction.java
@@ -41,17 +41,15 @@ import static org.hamcrest.Matchers.allOf;
  */
 
 public class DetoxAction {
-    private static final String LOG_TAG = "detox";
-
     private DetoxAction() {
         // static class
     }
 
-    public static ViewAction multiClick(int times) {
-        return actionWithAssertions(new GeneralClickAction(new DetoxMultiTap(times), GeneralLocation.CENTER, Press.FINGER, 0, 0));
+    public static ViewAction multiClick(int times, boolean strictMode) {
+        return actionWithAssertions(new GeneralClickAction(new DetoxMultiTap(strictMode, times), GeneralLocation.CENTER, Press.FINGER, 0, 0));
     }
 
-    public static ViewAction tapAtLocation(final int x, final int y) {
+    public static ViewAction tapAtLocation(final int x, final int y, boolean strictMode) {
         final int px = UiAutomatorHelper.convertDiptoPix(x);
         final int py = UiAutomatorHelper.convertDiptoPix(y);
         CoordinatesProvider c = new CoordinatesProvider() {
@@ -64,7 +62,7 @@ public class DetoxAction {
                 return new float[] {fx, fy};
             }
         };
-        return actionWithAssertions(new RNClickAction(c));
+        return actionWithAssertions(new RNClickAction(c, strictMode));
     }
 
     /**
diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/DetoxViewActions.java b/detox/android/detox/src/main/java/com/wix/detox/espresso/DetoxViewActions.java
index bfbf322dd..e20fafc23 100644
--- a/detox/android/detox/src/main/java/com/wix/detox/espresso/DetoxViewActions.java
+++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/DetoxViewActions.java
@@ -12,8 +12,8 @@ import static androidx.test.espresso.action.ViewActions.actionWithAssertions;
  * An alternative to {@link ViewActions} - providing alternative implementations, where needed.
  */
 public class DetoxViewActions {
-    public static ViewAction click() {
-        return actionWithAssertions(new RNClickAction());
+    public static ViewAction click(boolean strictMode) {
+        return actionWithAssertions(new RNClickAction(strictMode));
     }
 
     public static ViewAction typeText(String text) {
diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/action/DetoxMultiTap.kt b/detox/android/detox/src/main/java/com/wix/detox/espresso/action/DetoxMultiTap.kt
index d1a858f76..90af689c5 100644
--- a/detox/android/detox/src/main/java/com/wix/detox/espresso/action/DetoxMultiTap.kt
+++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/action/DetoxMultiTap.kt
@@ -1,8 +1,10 @@
 package com.wix.detox.espresso.action
 
+import android.util.Log
 import android.view.MotionEvent
 import androidx.test.espresso.UiController
 import androidx.test.espresso.action.Tapper
+import com.wix.detox.Detox
 import com.wix.detox.common.DetoxErrors.DetoxIllegalStateException
 import com.wix.detox.common.collect.PairsIterator
 import com.wix.detox.common.proxy.CallInfo
@@ -35,9 +37,12 @@ open class DetoxMultiTap
             private val coolDownTimeMs: Long = getPostTapCoolDownTime(),
             private val longTapMinTimeMs: Long = getLongTapMinTime(),
             private val tapEvents: TapEvents = TapEvents(),
-            private val uiControllerCallSpy: UiControllerSpy = UiControllerSpy.instance)
+            private val uiControllerCallSpy: UiControllerSpy = UiControllerSpy.instance,
+            private val strictMode: Boolean = true)
     : Tapper {
 
+    constructor(strictMode: Boolean, times: Int): this(times, strictMode = strictMode)
+
     override fun sendTap(uiController: UiController?, coordinates: FloatArray?, precision: FloatArray?)
             = sendTap(uiController, coordinates, precision, 0, 0)
 
@@ -103,7 +108,11 @@ open class DetoxMultiTap
     private fun verifyTapEventTimes(upEvent: CallInfo, downEvent: CallInfo) {
         val delta: Long = (upEvent - downEvent)!!
         if (delta >= longTapMinTimeMs) {
-            throw DetoxIllegalStateException("Tap handled too slowly, and turned into a long-tap!")
+            val message = "Tap handled too slowly, and has turned into a long-tap, instead!!!";
+            if (strictMode) {
+                throw DetoxIllegalStateException(message)
+            }
+            Log.w(Detox.LOG_TAG, message)
         }
     }
 }
diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/action/DetoxSingleTap.kt b/detox/android/detox/src/main/java/com/wix/detox/espresso/action/DetoxSingleTap.kt
index 53e30ecdc..f5e30fde3 100644
--- a/detox/android/detox/src/main/java/com/wix/detox/espresso/action/DetoxSingleTap.kt
+++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/action/DetoxSingleTap.kt
@@ -1,3 +1,3 @@
 package com.wix.detox.espresso.action
 
-open class DetoxSingleTap : DetoxMultiTap(1)
+open class DetoxSingleTap(strictMode: Boolean) : DetoxMultiTap(1, strictMode = strictMode)
diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/action/DetoxTypeTextAction.java b/detox/android/detox/src/main/java/com/wix/detox/espresso/action/DetoxTypeTextAction.java
index 3c100c4aa..7f27f6ca2 100644
--- a/detox/android/detox/src/main/java/com/wix/detox/espresso/action/DetoxTypeTextAction.java
+++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/action/DetoxTypeTextAction.java
@@ -17,7 +17,7 @@ public class DetoxTypeTextAction implements ViewAction {
 
     public DetoxTypeTextAction(String text) {
         this.text = text;
-        clickAction = new RNClickAction();
+        clickAction = new RNClickAction(false);
         typeTextAction = new TypeTextAction(text, false);
     }
 
diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/action/RNClickAction.java b/detox/android/detox/src/main/java/com/wix/detox/espresso/action/RNClickAction.java
index 68bf03906..f4ae4cd76 100644
--- a/detox/android/detox/src/main/java/com/wix/detox/espresso/action/RNClickAction.java
+++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/action/RNClickAction.java
@@ -20,18 +20,13 @@ import static androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
 public class RNClickAction implements ViewAction {
     private final GeneralClickAction clickAction;
 
-    public RNClickAction() {
-        clickAction = new GeneralClickAction(
-                        new DetoxSingleTap(),
-                        GeneralLocation.VISIBLE_CENTER,
-                        Press.FINGER,
-                        InputDevice.SOURCE_UNKNOWN,
-                        MotionEvent.BUTTON_PRIMARY);
+    public RNClickAction(boolean strictMode) {
+        this(GeneralLocation.VISIBLE_CENTER, strictMode);
     }
 
-    public RNClickAction(CoordinatesProvider coordinatesProvider) {
+    public RNClickAction(CoordinatesProvider coordinatesProvider, boolean strictMode) {
         clickAction = new GeneralClickAction(
-                        new DetoxSingleTap(),
+                        new DetoxSingleTap(strictMode),
                         coordinatesProvider,
                         Press.FINGER,
                         InputDevice.SOURCE_UNKNOWN,
diff --git a/detox/src/android/espressoapi/DetoxAction.js b/detox/src/android/espressoapi/DetoxAction.js
index a90aae936..0a98a6a26 100644
--- a/detox/src/android/espressoapi/DetoxAction.js
+++ b/detox/src/android/espressoapi/DetoxAction.js
@@ -34,8 +34,9 @@ function sanitize_android_direction(direction) {
   }
 } 
 class DetoxAction {
-  static multiClick(times) {
+  static multiClick(times, strictMode) {
     if (typeof times !== "number") throw new Error("times should be a number, but got " + (times + (" (" + (typeof times + ")"))));
+    if (typeof strictMode !== "boolean") throw new Error("strictMode should be a boolean, but got " + (strictMode + (" (" + (typeof strictMode + ")"))));
     return {
       target: {
         type: "Class",
@@ -45,13 +46,17 @@ class DetoxAction {
       args: [{
         type: "Integer",
         value: times
+      }, {
+        type: "boolean",
+        value: strictMode
       }]
     };
   }
 
-  static tapAtLocation(x, y) {
+  static tapAtLocation(x, y, strictMode) {
     if (typeof x !== "number") throw new Error("x should be a number, but got " + (x + (" (" + (typeof x + ")"))));
     if (typeof y !== "number") throw new Error("y should be a number, but got " + (y + (" (" + (typeof y + ")"))));
+    if (typeof strictMode !== "boolean") throw new Error("strictMode should be a boolean, but got " + (strictMode + (" (" + (typeof strictMode + ")"))));
     return {
       target: {
         type: "Class",
@@ -64,6 +69,9 @@ class DetoxAction {
       }, {
         type: "Integer",
         value: y
+      }, {
+        type: "boolean",
+        value: strictMode
       }]
     };
   }
diff --git a/detox/src/android/espressoapi/DetoxViewActions.js b/detox/src/android/espressoapi/DetoxViewActions.js
index 6832d1029..d584fe97e 100644
--- a/detox/src/android/espressoapi/DetoxViewActions.js
+++ b/detox/src/android/espressoapi/DetoxViewActions.js
@@ -7,14 +7,18 @@
 
 
 class DetoxViewActions {
-  static click() {
+  static click(strictMode) {
+    if (typeof strictMode !== "boolean") throw new Error("strictMode should be a boolean, but got " + (strictMode + (" (" + (typeof strictMode + ")"))));
     return {
       target: {
         type: "Class",
         value: "com.wix.detox.espresso.DetoxViewActions"
       },
       method: "click",
-      args: []
+      args: [{
+        type: "boolean",
+        value: strictMode
+      }]
     };
   }
 
diff --git a/detox/src/android/expect.js b/detox/src/android/expect.js
index 82f02eb9a..4f7dbddc5 100644
--- a/detox/src/android/expect.js
+++ b/detox/src/android/expect.js
@@ -29,16 +29,20 @@ class Action {
 }
 
 class TapAction extends Action {
-  constructor(value) {
+  constructor(value = {}) {
     super();
-    this._call = invoke.callDirectly(value ? DetoxActionApi.tapAtLocation(value.x, value.y) : DetoxViewActionsApi.click());
+
+    const { strict = true } = value;
+    this._call = invoke.callDirectly((value.x && value.y) ? DetoxActionApi.tapAtLocation(value.x, value.y, strict) : DetoxViewActionsApi.click(strict));
   }
 }
 
 class TapAtPointAction extends Action {
   constructor(value) {
     super();
-    this._call = invoke.callDirectly(DetoxActionApi.tapAtLocation(value.x, value.y));
+
+    const { strict = true } = value;
+    this._call = invoke.callDirectly(DetoxActionApi.tapAtLocation(value.x, value.y, strict));
   }
 }
 
@@ -50,9 +54,11 @@ class LongPressAction extends Action {
 }
 
 class MultiClickAction extends Action {
-  constructor(times) {
+  constructor(times, value = {}) {
     super();
-    this._call = invoke.callDirectly(DetoxActionApi.multiClick(times));
+
+    const { strict = true } = value;
+    this._call = invoke.callDirectly(DetoxActionApi.multiClick(times, strict));
   }
 }
 
@@ -241,8 +247,8 @@ class Element {
     return await new ActionInteraction(this._invocationManager, this, new LongPressAction()).execute();
   }
 
-  async multiTap(times) {
-    return await new ActionInteraction(this._invocationManager, this, new MultiClickAction(times)).execute();
+  async multiTap(times, value) {
+    return await new ActionInteraction(this._invocationManager, this, new MultiClickAction(times, value)).execute();
   }
 
   async tapBackspaceKey() {


Test patch:
diff --git a/detox/android/detox/src/test/java/com/wix/detox/espresso/action/DetoxMultiTapSpec.kt b/detox/android/detox/src/test/java/com/wix/detox/espresso/action/DetoxMultiTapSpec.kt
index b1b42956f..1b6de5d89 100644
--- a/detox/android/detox/src/test/java/com/wix/detox/espresso/action/DetoxMultiTapSpec.kt
+++ b/detox/android/detox/src/test/java/com/wix/detox/espresso/action/DetoxMultiTapSpec.kt
@@ -63,6 +63,7 @@ object DetoxMultiTapSpec: Spek({
                 whenever(uiControllerCallSpy.eventInjectionsIterator()).thenReturn(injectionsHistory.iterator())
 
         fun uut(times: Int) = DetoxMultiTap(times, interTapsDelayMs, coolDownTimeMs, longTapMinTimeMs, tapEvents, uiControllerCallSpy)
+        fun uutNonStrict(times: Int) = DetoxMultiTap(times, interTapsDelayMs, coolDownTimeMs, longTapMinTimeMs, tapEvents, uiControllerCallSpy, false)
         fun sendOneTap(uut: DetoxMultiTap = uut(1)) = uut.sendTap(uiController, coordinates, precision, -1, -1)
         fun sendTwoTaps(uut: DetoxMultiTap = uut(2)) = uut.sendTap(uiController, coordinates, precision, -1, -1)
 
@@ -210,5 +211,17 @@ object DetoxMultiTapSpec: Spek({
                 sendOneTap()
             }
         }
+
+        it("should NOT throw even though ui-controller spy indicates tap has turned into a long-tap, in non-strict mode") {
+            givenInjectionSuccess()
+
+            val injectionsHistory = listOf(
+                    CallInfo(longTapMinTimeMs - 1, longTapMinTimeMs),
+                    CallInfo(0, 1)
+            )
+            givenInjectionCallsHistory(injectionsHistory)
+            uutNonStrict(1).sendTap(uiController, coordinates, precision, -1, -1)
+        }
+
     }
 })
diff --git a/detox/src/android/expect.test.js b/detox/src/android/expect.test.js
index a3f5369a6..9b59b14b3 100644
--- a/detox/src/android/expect.test.js
+++ b/detox/src/android/expect.test.js
@@ -142,10 +142,13 @@ describe('expect', () => {
   describe('element interactions', () => {
     it('should tap and long-press', async () => {
       await e.element(e.by.label('Tap Me')).tap();
+      await e.element(e.by.label('Tap Me')).tap({ strict: false });
       await e.element(e.by.label('Tap Me')).tap({ x: 10, y: 10 });
       await e.element(e.by.label('Tap Me')).tapAtPoint({x: 100, y: 200});
+      await e.element(e.by.label('Tap Me')).tapAtPoint({x: 100, y: 200, strict: false});
       await e.element(e.by.label('Tap Me')).longPress();
       await e.element(e.by.id('UniqueId819')).multiTap(3);
+      await e.element(e.by.id('UniqueId819')).multiTap(3, { strict: false });
     });
 
     it('should not tap and long-press given bad args', async () => {
diff --git a/detox/test/e2e/03.actions.test.js b/detox/test/e2e/03.actions.test.js
index 973b67544..cffc6d62c 100644
--- a/detox/test/e2e/03.actions.test.js
+++ b/detox/test/e2e/03.actions.test.js
@@ -72,15 +72,18 @@ describe('Actions', () => {
       await driver.sluggishTapElement.tap();
     } catch (e) {
       console.log('Got an expected error', e);
-      if (!e.toString().includes('Tap handled too slowly, and turned into a long-tap!')) {
+      if (!e.toString().includes('Tap handled too slowly, and has turned into a long-tap, instead!!!')) {
         throw new Error('Error content isn\'t as expected!');
       }
       return;
     }
-
     throw new Error('Expected an error');
   });
 
+  it(':android: should NOT throw if tap handling is too slow, if strict-mode is disabled', async () => {
+    await driver.sluggishTapElement.tap({ strict: false });
+  });
+
   it('should type in an element', async () => {
     const typedText = device.getPlatform() === 'ios' ? 'Type Working 123 אֱבּג абв!!!' : "Type Working!!!";
     await element(by.id('UniqueId937')).typeText(typedText);
diff --git a/detox/test/e2e/drivers/actions-driver.js b/detox/test/e2e/drivers/actions-driver.js
index 9f07007ed..774f41567 100644
--- a/detox/test/e2e/drivers/actions-driver.js
+++ b/detox/test/e2e/drivers/actions-driver.js
@@ -37,7 +37,7 @@ const driver = {
 
   sluggishTapElement: {
     testId: 'sluggishTappableText',
-    tap: () => element(by.id(driver.sluggishTapElement.testId)).tap()
+    tap: (value) => element(by.id(driver.sluggishTapElement.testId)).tap(value)
   }
 };