From 89961f57b6a3e5717b98437e26bea969ffd985b2 Mon Sep 17 00:00:00 2001 From: Georgy Steshin Date: Tue, 2 Jan 2024 14:56:44 +0200 Subject: [PATCH 1/5] Implementation of scrolling fix for Android 33/34 --- .../com/wix/detox/espresso/DeviceDisplay.kt | 2 +- .../detox/espresso/scroll/ScrollHelper.java | 49 +++++++++- .../detox/espresso/scroll/ScrollHelperTest.kt | 89 +++++++++++++++++++ 3 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/ScrollHelperTest.kt diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/DeviceDisplay.kt b/detox/android/detox/src/main/java/com/wix/detox/espresso/DeviceDisplay.kt index 72012b181c..f882edeba4 100644 --- a/detox/android/detox/src/main/java/com/wix/detox/espresso/DeviceDisplay.kt +++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/DeviceDisplay.kt @@ -17,7 +17,7 @@ object DeviceDisplay { } @JvmStatic - fun getScreenSizeInPX(): FloatArray? { + fun getScreenSizeInPX(): FloatArray { val metrics = getDisplayMetrics() return floatArrayOf(metrics.widthPixels.toFloat(), metrics.heightPixels.toFloat()) } diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/scroll/ScrollHelper.java b/detox/android/detox/src/main/java/com/wix/detox/espresso/scroll/ScrollHelper.java index ac08100841..05b600401e 100644 --- a/detox/android/detox/src/main/java/com/wix/detox/espresso/scroll/ScrollHelper.java +++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/scroll/ScrollHelper.java @@ -1,10 +1,13 @@ package com.wix.detox.espresso.scroll; import android.content.Context; +import android.graphics.Insets; import android.graphics.Point; +import android.os.Build; import android.util.Log; import android.view.View; import android.view.ViewConfiguration; +import android.view.WindowInsets; import com.wix.detox.action.common.MotionDir; import com.wix.detox.espresso.DeviceDisplay; @@ -171,6 +174,50 @@ private static Point getScrollStartPoint(View view, @MotionDir int direction, Fl int offsetX = ((int) (view.getWidth() * offsetFactorX) + safetyOffsetX); int offsetY = ((int) (view.getHeight() * offsetFactorY) + safetyOffsetY); + final float[] displaySize = DeviceDisplay.getScreenSizeInPX(); + + // Calculate the min/max scrollable area, taking into account the system gesture insets. + // By default we assume the scrollable area is the entire view. + int gestureSafeOffset = DeviceDisplay.convertDpiToPx(1) * 3; + int minX = gestureSafeOffset; + int minY = gestureSafeOffset; + float maxX = displaySize[0] - gestureSafeOffset; + float maxY = displaySize[1] - gestureSafeOffset; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // System gesture insets are only available on Android Q (29) and above. + WindowInsets rootWindowInsets = view.getRootWindowInsets(); + if (rootWindowInsets == null) { + Log.w(LOG_TAG, "Could not get root window insets. Using default calculation."); + } else { + Insets gestureInsets = rootWindowInsets.getSystemGestureInsets(); + minX = gestureInsets.left; + minY = gestureInsets.top; + maxX -= gestureInsets.right; + maxY -= gestureInsets.bottom; + + Log.d(LOG_TAG, + "System gesture insets: " + + gestureInsets + " minX=" + + minX + " minY=" + minY + " maxX=" + maxX + " maxY=" + maxY + " offsetX=" + offsetX + " offsetY=" + offsetY); + } + } + + switch (direction) { + case MOTION_DIR_UP: + offsetY = (int) Math.max(offsetY, minY); + break; + case MOTION_DIR_DOWN: + offsetY = (int) Math.min(offsetY, maxY); + break; + case MOTION_DIR_LEFT: + offsetX = (int) Math.max(offsetX, minX); + break; + case MOTION_DIR_RIGHT: + offsetX = (int) Math.min(offsetX, maxX); + break; + } + point.offset(offsetX, offsetY); return point; } @@ -217,7 +264,7 @@ private static Point getGlobalViewLocation(View view) { return new Point(pos[0], pos[1]); } - private static ViewConfiguration getViewConfiguration() { + public static ViewConfiguration getViewConfiguration() { if (viewConfiguration == null) { final Context applicationContext = InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext(); viewConfiguration = ViewConfiguration.get(applicationContext); diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/ScrollHelperTest.kt b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/ScrollHelperTest.kt new file mode 100644 index 0000000000..3ee94bd343 --- /dev/null +++ b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/ScrollHelperTest.kt @@ -0,0 +1,89 @@ +package com.wix.detox.espresso.scroll + +import android.graphics.Insets +import android.view.MotionEvent +import android.view.View +import android.view.WindowInsets +import androidx.test.espresso.UiController +import androidx.test.platform.app.InstrumentationRegistry +import com.wix.detox.action.common.MOTION_DIR_DOWN +import com.wix.detox.espresso.DeviceDisplay +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import kotlin.test.assertEquals + +@Config(qualifiers = "xxxhdpi", sdk = [33]) +@RunWith(RobolectricTestRunner::class) +class ScrollHelperTest { + + private val display = DeviceDisplay.getScreenSizeInPX() + private val displayWidth = display[0].toInt() + private val displayHeight = display[1].toInt() + + private val uiController = mock() + private val view = mockView(displayWidth, displayHeight) + + @Test + fun `perform scroll down for 200 dp on full screen view`() { + val amountInDp = 200.0 + val amountInPx = amountInDp * DeviceDisplay.getDensity() + val touchSlopPx = ScrollHelper.getViewConfiguration().scaledTouchSlop + + ScrollHelper.perform(uiController, view, MOTION_DIR_DOWN, amountInDp, null, null) + val capture = argumentCaptor>() + verify(uiController).injectMotionEventSequence(capture.capture()) + + val listOfCapturedEvents = capture.firstValue.toList() + val lastEvent = listOfCapturedEvents.last() // The last event is the UP event with the target coordinates + // the joinery of the swipe is not interesting + val lastEventX = lastEvent.x + val lastEventY = lastEvent.y + assertEquals(displayWidth / 2.0, lastEventX.toDouble(), 0.0) + assertEquals(displayHeight - amountInPx - touchSlopPx - DeviceDisplay.convertDpiToPx(1.0), lastEventY.toDouble(), 0.0) + } + + @Test + fun `perform scroll down for 200 dp on full screen view with offset y`() { + val amountInDp = 200.0 + val amountInPx = amountInDp * DeviceDisplay.getDensity() + val touchSlopPx = ScrollHelper.getViewConfiguration().scaledTouchSlop + val offsetPercent = 0.9f + + ScrollHelper.perform(uiController, view, MOTION_DIR_DOWN, amountInDp, null, offsetPercent) + val capture = argumentCaptor>() + verify(uiController).injectMotionEventSequence(capture.capture()) + + val listOfCapturedEvents = capture.firstValue.toList() + val lastEvent = listOfCapturedEvents.last() // The last event is the UP event with the target coordinates + // the joinery of the swipe is not interesting + val lastEventX = lastEvent.x + val lastEventY = lastEvent.y + assertEquals(displayWidth / 2.0, lastEventX.toDouble(), 0.0) + assertEquals(displayHeight - amountInPx - touchSlopPx - DeviceDisplay.convertDpiToPx(1.0), lastEventY.toDouble(), 0.0) + + } + + private fun mockView(displayWidth: Int, displayHeight: Int): View { + val windowInsets = mock() { + whenever(it.systemGestureInsets).thenReturn( + Insets.of(88, 88, 88, 100) + ) + } + + val view = mock() { + whenever(it.width).thenReturn(displayWidth) + whenever(it.height).thenReturn(displayHeight) + whenever(it.canScrollVertically(any())).thenReturn(true) // We allow endless scroll + whenever(it.context).thenReturn(InstrumentationRegistry.getInstrumentation().targetContext) + whenever(it.rootWindowInsets).thenReturn(windowInsets) + } + return view + } +} From 871306abc219c4738c44b1a7ed10df569d587979 Mon Sep 17 00:00:00 2001 From: Georgy Steshin Date: Thu, 4 Jan 2024 16:56:37 +0200 Subject: [PATCH 2/5] Fixing scroll interfere with system gesture navigation on android 33-34 --- .../detox/espresso/scroll/ScrollHelper.java | 116 +++++++++++++----- .../detox/espresso/scroll/ScrollHelperTest.kt | 73 ++++++----- 2 files changed, 125 insertions(+), 64 deletions(-) diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/scroll/ScrollHelper.java b/detox/android/detox/src/main/java/com/wix/detox/espresso/scroll/ScrollHelper.java index 05b600401e..b61d292c89 100644 --- a/detox/android/detox/src/main/java/com/wix/detox/espresso/scroll/ScrollHelper.java +++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/scroll/ScrollHelper.java @@ -12,6 +12,7 @@ import com.wix.detox.action.common.MotionDir; import com.wix.detox.espresso.DeviceDisplay; +import androidx.annotation.VisibleForTesting; import androidx.test.espresso.UiController; import androidx.test.platform.app.InstrumentationRegistry; @@ -54,7 +55,7 @@ public static void perform(UiController uiController, View view, @MotionDir int final int times = amountInPx / safeScrollableRangePx; final int remainder = amountInPx % safeScrollableRangePx; - Log.d(LOG_TAG, "prescroll amountDP="+amountInDP + " amountPx="+amountInPx + " scrollableRangePx="+safeScrollableRangePx + " times="+times + " remainder="+remainder); + Log.d(LOG_TAG, "prescroll amountDP=" + amountInDP + " amountPx=" + amountInPx + " scrollableRangePx=" + safeScrollableRangePx + " times=" + times + " remainder=" + remainder); for (int i = 0; i < times; ++i) { scrollOnce(uiController, view, direction, safeScrollableRangePx, startOffsetPercentX, startOffsetPercentY); @@ -118,25 +119,32 @@ private static void waitForFlingToFinish(View view, UiController uiController) { } } - private static int getViewSafeScrollableRangePix(View view, @MotionDir int direction) { + @VisibleForTesting + public static int getViewSafeScrollableRangePix(View view, @MotionDir int direction) { final float[] screenSize = DeviceDisplay.getScreenSizeInPX(); final int[] pos = new int[2]; view.getLocationInWindow(pos); int range; switch (direction) { - case MOTION_DIR_LEFT: range = (int) ((screenSize[0] - pos[0]) * SCROLL_RANGE_SAFE_PERCENT); break; - case MOTION_DIR_RIGHT: range = (int) ((pos[0] + view.getWidth()) * SCROLL_RANGE_SAFE_PERCENT); break; - case MOTION_DIR_UP: range = (int) ((screenSize[1] - pos[1]) * SCROLL_RANGE_SAFE_PERCENT); break; - default: range = (int) ((pos[1] + view.getHeight()) * SCROLL_RANGE_SAFE_PERCENT); break; + case MOTION_DIR_LEFT: + range = (int) ((screenSize[0] - pos[0]) * SCROLL_RANGE_SAFE_PERCENT); + break; + case MOTION_DIR_RIGHT: + range = (int) ((pos[0] + view.getWidth()) * SCROLL_RANGE_SAFE_PERCENT); + break; + case MOTION_DIR_UP: + range = (int) ((screenSize[1] - pos[1]) * SCROLL_RANGE_SAFE_PERCENT); + break; + default: + range = (int) ((pos[1] + view.getHeight()) * SCROLL_RANGE_SAFE_PERCENT); + break; } return range; } - private static Point getScrollStartPoint(View view, @MotionDir int direction, Float startOffsetPercentX, Float startOffsetPercentY) { + private static int[] getScrollStartCoordinatesInView(View view, @MotionDir int direction, Float startOffsetPercentX, Float startOffsetPercentY) { final int safetyOffset = DeviceDisplay.convertDpiToPx(1); - - Point point = getGlobalViewLocation(view); float offsetFactorX; float offsetFactorY; int safetyOffsetX; @@ -174,52 +182,87 @@ private static Point getScrollStartPoint(View view, @MotionDir int direction, Fl int offsetX = ((int) (view.getWidth() * offsetFactorX) + safetyOffsetX); int offsetY = ((int) (view.getHeight() * offsetFactorY) + safetyOffsetY); + return new int[]{offsetX, offsetY}; + } + + /** + * Calculates the scroll start point, with respect to the global screen coordinates and gesture insets. + * @param view The view to scroll. + * @param direction The scroll direction. + * @param startOffsetPercentX The scroll start offset, as a percentage of the view's width. Null means select automatically. + * @param startOffsetPercentY The scroll start offset, as a percentage of the view's height. Null means select automatically. + * @return a Point object, denoting the scroll start point. + */ + private static Point getScrollStartPoint(View view, @MotionDir int direction, Float startOffsetPercentX, Float startOffsetPercentY) { + Point result = getGlobalViewLocation(view); + + // 1. Calculate the scroll start point, with respect to the view's location. + int[] coordinates = getScrollStartCoordinatesInView(view, direction, startOffsetPercentX, startOffsetPercentY); + + // 2. Make sure that the start point is within the scrollable area, taking into account the system gesture insets. + coordinates = calculateScrollStartWithNavigationGestureInsets(view, direction, coordinates[0], coordinates[1]); + + result.offset(coordinates[0], coordinates[1]); + return result; + } + + /** + * Calculates the scroll start point, with respect to the system gesture insets. + * @param view + * @param direction The scroll direction. + * @param x The scroll start point, with respect to the view's location. + * @param y The scroll start point, with respect to the view's location. + * @return an array of two integers, denoting the scroll start point, with respect to the system gesture insets. + */ + private static int[] calculateScrollStartWithNavigationGestureInsets(View view, int direction, int x, int y) { + // System gesture insets are only available on Android Q (29) and above. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + return new int[]{x, y}; + } + final float[] displaySize = DeviceDisplay.getScreenSizeInPX(); // Calculate the min/max scrollable area, taking into account the system gesture insets. - // By default we assume the scrollable area is the entire view. - int gestureSafeOffset = DeviceDisplay.convertDpiToPx(1) * 3; + // By default we assume the scrollable area is the entire screen. + // 2dp is a safety offset to make sure we don't hit the system gesture area. + int gestureSafeOffset = DeviceDisplay.convertDpiToPx(2); int minX = gestureSafeOffset; int minY = gestureSafeOffset; float maxX = displaySize[0] - gestureSafeOffset; float maxY = displaySize[1] - gestureSafeOffset; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - // System gesture insets are only available on Android Q (29) and above. - WindowInsets rootWindowInsets = view.getRootWindowInsets(); - if (rootWindowInsets == null) { - Log.w(LOG_TAG, "Could not get root window insets. Using default calculation."); - } else { - Insets gestureInsets = rootWindowInsets.getSystemGestureInsets(); - minX = gestureInsets.left; - minY = gestureInsets.top; - maxX -= gestureInsets.right; - maxY -= gestureInsets.bottom; - - Log.d(LOG_TAG, - "System gesture insets: " + - gestureInsets + " minX=" + - minX + " minY=" + minY + " maxX=" + maxX + " maxY=" + maxY + " offsetX=" + offsetX + " offsetY=" + offsetY); - } + // Try to get the root window insets, and if available, use them to calculate the scrollable area. + WindowInsets rootWindowInsets = view.getRootWindowInsets(); + if (rootWindowInsets == null) { + Log.w(LOG_TAG, "Could not get root window insets"); + } else { + Insets gestureInsets = rootWindowInsets.getSystemGestureInsets(); + minX = gestureInsets.left; + minY = gestureInsets.top; + maxX -= gestureInsets.right; + maxY -= gestureInsets.bottom; + + Log.d(LOG_TAG, + "System gesture insets: " + + gestureInsets + " minX=" + minX + " minY=" + minY + " maxX=" + maxX + " maxY=" + maxY + " currentX=" + x + " currentY=" + y); } switch (direction) { case MOTION_DIR_UP: - offsetY = (int) Math.max(offsetY, minY); + y = (int) Math.max(y, minY); break; case MOTION_DIR_DOWN: - offsetY = (int) Math.min(offsetY, maxY); + y = (int) Math.min(y, maxY); break; case MOTION_DIR_LEFT: - offsetX = (int) Math.max(offsetX, minX); + x = (int) Math.max(x, minX); break; case MOTION_DIR_RIGHT: - offsetX = (int) Math.min(offsetX, maxX); + x = (int) Math.min(x, maxX); break; } - point.offset(offsetX, offsetY); - return point; + return new int[]{x, y}; } private static Point getScrollEndPoint(Point startPoint, @MotionDir int direction, int userAmountPx, Float startOffsetPercentX, Float startOffsetPercentY) { @@ -258,6 +301,11 @@ private static Point getScrollEndPoint(Point startPoint, @MotionDir int directio return point; } + /** + * Calculates the global location of the view on the screen. + * @param view The view to calculate. + * @return a Point object, denoting the global location of the view. + */ private static Point getGlobalViewLocation(View view) { int[] pos = new int[2]; view.getLocationInWindow(pos); diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/ScrollHelperTest.kt b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/ScrollHelperTest.kt index 3ee94bd343..67bf17bdc2 100644 --- a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/ScrollHelperTest.kt +++ b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/ScrollHelperTest.kt @@ -8,6 +8,7 @@ import androidx.test.espresso.UiController import androidx.test.platform.app.InstrumentationRegistry import com.wix.detox.action.common.MOTION_DIR_DOWN import com.wix.detox.espresso.DeviceDisplay +import com.wix.detox.espresso.scroll.ScrollHelper.getViewSafeScrollableRangePix import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any @@ -19,6 +20,8 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config import kotlin.test.assertEquals +private const val INSETS_SIZE = 100 + @Config(qualifiers = "xxxhdpi", sdk = [33]) @RunWith(RobolectricTestRunner::class) class ScrollHelperTest { @@ -26,54 +29,64 @@ class ScrollHelperTest { private val display = DeviceDisplay.getScreenSizeInPX() private val displayWidth = display[0].toInt() private val displayHeight = display[1].toInt() + private val touchSlopPx = ScrollHelper.getViewConfiguration().scaledTouchSlop + private val safetyMarginPx = DeviceDisplay.convertDpiToPx(2.0) - private val uiController = mock() - private val view = mockView(displayWidth, displayHeight) + private val uiControllerMock = mock() + private val viewMock = mockViewWithGestureNavigation(displayWidth, displayHeight) @Test - fun `perform scroll down for 200 dp on full screen view`() { + fun `perform scroll down for 200 dp on full screen view with gesture navigation enabled`() { val amountInDp = 200.0 val amountInPx = amountInDp * DeviceDisplay.getDensity() - val touchSlopPx = ScrollHelper.getViewConfiguration().scaledTouchSlop - ScrollHelper.perform(uiController, view, MOTION_DIR_DOWN, amountInDp, null, null) - val capture = argumentCaptor>() - verify(uiController).injectMotionEventSequence(capture.capture()) + // Perform the scroll + ScrollHelper.perform(uiControllerMock, viewMock, MOTION_DIR_DOWN, amountInDp, null, null) - val listOfCapturedEvents = capture.firstValue.toList() - val lastEvent = listOfCapturedEvents.last() // The last event is the UP event with the target coordinates - // the joinery of the swipe is not interesting - val lastEventX = lastEvent.x - val lastEventY = lastEvent.y - assertEquals(displayWidth / 2.0, lastEventX.toDouble(), 0.0) - assertEquals(displayHeight - amountInPx - touchSlopPx - DeviceDisplay.convertDpiToPx(1.0), lastEventY.toDouble(), 0.0) + // Verify start and end coordinates + val upEvent = getUpEvent() + val x = upEvent.x + val y = upEvent.y + // Verify that the scroll started at the center of the view + assertEquals(displayWidth / 2.0, x.toDouble(), 0.0) + // Verify that the scroll ended at the center of the view minus the requested amount + assertEquals(displayHeight - amountInPx - touchSlopPx - safetyMarginPx - INSETS_SIZE, y.toDouble(), 0.0) } @Test - fun `perform scroll down for 200 dp on full screen view with offset y`() { - val amountInDp = 200.0 - val amountInPx = amountInDp * DeviceDisplay.getDensity() - val touchSlopPx = ScrollHelper.getViewConfiguration().scaledTouchSlop - val offsetPercent = 0.9f + fun `perform scroll down to edge on full screen view with gesture navigation enabled`() { + ScrollHelper.performOnce(uiControllerMock, viewMock, MOTION_DIR_DOWN) + val upEvent = getUpEvent() + val x = upEvent.x + val y = upEvent.y + val amountInPx = getViewSafeScrollableRangePix(viewMock, MOTION_DIR_DOWN).toFloat() - ScrollHelper.perform(uiController, view, MOTION_DIR_DOWN, amountInDp, null, offsetPercent) + assertEquals(displayWidth / 2.0, x.toDouble(), 0.0) + assertEquals(displayHeight - amountInPx - touchSlopPx - safetyMarginPx - INSETS_SIZE, y, 0.0f) + + } + + /** + * Get the performed UP event from the ui controller + */ + private fun getUpEvent(): MotionEvent { val capture = argumentCaptor>() - verify(uiController).injectMotionEventSequence(capture.capture()) + // Capture the events from the ui controller + verify(uiControllerMock).injectMotionEventSequence(capture.capture()) val listOfCapturedEvents = capture.firstValue.toList() - val lastEvent = listOfCapturedEvents.last() // The last event is the UP event with the target coordinates - // the joinery of the swipe is not interesting - val lastEventX = lastEvent.x - val lastEventY = lastEvent.y - assertEquals(displayWidth / 2.0, lastEventX.toDouble(), 0.0) - assertEquals(displayHeight - amountInPx - touchSlopPx - DeviceDisplay.convertDpiToPx(1.0), lastEventY.toDouble(), 0.0) - + // The last event is the UP event with the target coordinates. All of the rest are not interesting + return listOfCapturedEvents.last() } - private fun mockView(displayWidth: Int, displayHeight: Int): View { + /** + * Mock a view with gesture navigation enabled + */ + private fun mockViewWithGestureNavigation(displayWidth: Int, displayHeight: Int): View { + // This is how we enable gesture navigation val windowInsets = mock() { whenever(it.systemGestureInsets).thenReturn( - Insets.of(88, 88, 88, 100) + Insets.of(INSETS_SIZE, INSETS_SIZE, INSETS_SIZE, INSETS_SIZE) ) } From e8c6698c477efd999ab3d5b01b6b8ff8836e67f5 Mon Sep 17 00:00:00 2001 From: Georgy Steshin Date: Mon, 8 Jan 2024 13:19:47 +0200 Subject: [PATCH 3/5] Fixes after MR --- .../detox/espresso/scroll/ScrollHelper.java | 8 ++++---- .../detox/espresso/scroll/ScrollHelperTest.kt | 18 ++++++------------ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/scroll/ScrollHelper.java b/detox/android/detox/src/main/java/com/wix/detox/espresso/scroll/ScrollHelper.java index b61d292c89..bef721c984 100644 --- a/detox/android/detox/src/main/java/com/wix/detox/espresso/scroll/ScrollHelper.java +++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/scroll/ScrollHelper.java @@ -143,7 +143,7 @@ public static int getViewSafeScrollableRangePix(View view, @MotionDir int direct return range; } - private static int[] getScrollStartCoordinatesInView(View view, @MotionDir int direction, Float startOffsetPercentX, Float startOffsetPercentY) { + private static int[] getScrollStartOffsetInView(View view, @MotionDir int direction, Float startOffsetPercentX, Float startOffsetPercentY) { final int safetyOffset = DeviceDisplay.convertDpiToPx(1); float offsetFactorX; float offsetFactorY; @@ -197,10 +197,10 @@ private static Point getScrollStartPoint(View view, @MotionDir int direction, Fl Point result = getGlobalViewLocation(view); // 1. Calculate the scroll start point, with respect to the view's location. - int[] coordinates = getScrollStartCoordinatesInView(view, direction, startOffsetPercentX, startOffsetPercentY); + int[] coordinates = getScrollStartOffsetInView(view, direction, startOffsetPercentX, startOffsetPercentY); // 2. Make sure that the start point is within the scrollable area, taking into account the system gesture insets. - coordinates = calculateScrollStartWithNavigationGestureInsets(view, direction, coordinates[0], coordinates[1]); + coordinates = applyScreenInsets(view, direction, coordinates[0], coordinates[1]); result.offset(coordinates[0], coordinates[1]); return result; @@ -214,7 +214,7 @@ private static Point getScrollStartPoint(View view, @MotionDir int direction, Fl * @param y The scroll start point, with respect to the view's location. * @return an array of two integers, denoting the scroll start point, with respect to the system gesture insets. */ - private static int[] calculateScrollStartWithNavigationGestureInsets(View view, int direction, int x, int y) { + private static int[] applyScreenInsets(View view, int direction, int x, int y) { // System gesture insets are only available on Android Q (29) and above. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { return new int[]{x, y}; diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/ScrollHelperTest.kt b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/ScrollHelperTest.kt index 67bf17bdc2..7876129fa1 100644 --- a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/ScrollHelperTest.kt +++ b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/ScrollHelperTest.kt @@ -36,33 +36,27 @@ class ScrollHelperTest { private val viewMock = mockViewWithGestureNavigation(displayWidth, displayHeight) @Test - fun `perform scroll down for 200 dp on full screen view with gesture navigation enabled`() { + fun `should take gesture navigation into account when scrolling down`() { val amountInDp = 200.0 val amountInPx = amountInDp * DeviceDisplay.getDensity() - // Perform the scroll ScrollHelper.perform(uiControllerMock, viewMock, MOTION_DIR_DOWN, amountInDp, null, null) - // Verify start and end coordinates val upEvent = getUpEvent() - val x = upEvent.x - val y = upEvent.y // Verify that the scroll started at the center of the view - assertEquals(displayWidth / 2.0, x.toDouble(), 0.0) + assertEquals(displayWidth / 2.0, upEvent.x.toDouble(), 0.0) // Verify that the scroll ended at the center of the view minus the requested amount - assertEquals(displayHeight - amountInPx - touchSlopPx - safetyMarginPx - INSETS_SIZE, y.toDouble(), 0.0) + assertEquals(displayHeight - amountInPx - touchSlopPx - safetyMarginPx - INSETS_SIZE, upEvent.y.toDouble(), 0.0) } @Test - fun `perform scroll down to edge on full screen view with gesture navigation enabled`() { + fun `should scroll down to edge on full screen view when gesture navigation enabled`() { ScrollHelper.performOnce(uiControllerMock, viewMock, MOTION_DIR_DOWN) val upEvent = getUpEvent() - val x = upEvent.x - val y = upEvent.y val amountInPx = getViewSafeScrollableRangePix(viewMock, MOTION_DIR_DOWN).toFloat() - assertEquals(displayWidth / 2.0, x.toDouble(), 0.0) - assertEquals(displayHeight - amountInPx - touchSlopPx - safetyMarginPx - INSETS_SIZE, y, 0.0f) + assertEquals(displayWidth / 2.0, upEvent.x.toDouble(), 0.0) + assertEquals(displayHeight - amountInPx - touchSlopPx - safetyMarginPx - INSETS_SIZE, upEvent.y, 0.0f) } From 374bd349e1c27a285b404b852e98eed6e34a2d05 Mon Sep 17 00:00:00 2001 From: Georgy Steshin Date: Tue, 9 Jan 2024 16:19:56 +0200 Subject: [PATCH 4/5] Added more tests --- .../detox/espresso/scroll/ScrollHelperTest.kt | 102 +++++++++++++++++- 1 file changed, 97 insertions(+), 5 deletions(-) diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/ScrollHelperTest.kt b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/ScrollHelperTest.kt index 7876129fa1..778781313b 100644 --- a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/ScrollHelperTest.kt +++ b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/ScrollHelperTest.kt @@ -7,8 +7,10 @@ import android.view.WindowInsets import androidx.test.espresso.UiController import androidx.test.platform.app.InstrumentationRegistry import com.wix.detox.action.common.MOTION_DIR_DOWN +import com.wix.detox.action.common.MOTION_DIR_LEFT +import com.wix.detox.action.common.MOTION_DIR_RIGHT +import com.wix.detox.action.common.MOTION_DIR_UP import com.wix.detox.espresso.DeviceDisplay -import com.wix.detox.espresso.scroll.ScrollHelper.getViewSafeScrollableRangePix import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any @@ -21,8 +23,12 @@ import org.robolectric.annotation.Config import kotlin.test.assertEquals private const val INSETS_SIZE = 100 +private const val SCROLL_RANGE_SAFE_PERCENT = 0.9f // ScrollHelper.SCROLL_RANGE_SAFE_PERCENT -@Config(qualifiers = "xxxhdpi", sdk = [33]) +@Config( + qualifiers = "xxxhdpi", // 1280x1880 + sdk = [33] +) @RunWith(RobolectricTestRunner::class) class ScrollHelperTest { @@ -36,7 +42,7 @@ class ScrollHelperTest { private val viewMock = mockViewWithGestureNavigation(displayWidth, displayHeight) @Test - fun `should take gesture navigation into account when scrolling down`() { + fun `should scrolling down by 200 when gesture navigation enabled`() { val amountInDp = 200.0 val amountInPx = amountInDp * DeviceDisplay.getDensity() @@ -49,15 +55,81 @@ class ScrollHelperTest { assertEquals(displayHeight - amountInPx - touchSlopPx - safetyMarginPx - INSETS_SIZE, upEvent.y.toDouble(), 0.0) } + @Test + fun `should scrolling down by 200 when gesture navigation disabled`() { + val amountInDp = 200.0 + val amountInPx = amountInDp * DeviceDisplay.getDensity() + + val viewMock = mockViewWithoutGestureNavigation(displayWidth, displayHeight) + ScrollHelper.perform(uiControllerMock, viewMock, MOTION_DIR_DOWN, amountInDp, null, null) + + val upEvent = getUpEvent() + // Verify that the scroll started at the center of the view + assertEquals(displayWidth / 2.0, upEvent.x.toDouble(), 0.0) + // Verify that the scroll ended at the center of the view minus the requested amount + assertEquals(displayHeight - amountInPx - touchSlopPx - safetyMarginPx, upEvent.y.toDouble(), 0.0) + } + @Test fun `should scroll down to edge on full screen view when gesture navigation enabled`() { ScrollHelper.performOnce(uiControllerMock, viewMock, MOTION_DIR_DOWN) val upEvent = getUpEvent() - val amountInPx = getViewSafeScrollableRangePix(viewMock, MOTION_DIR_DOWN).toFloat() + val amountInPx = displayHeight * SCROLL_RANGE_SAFE_PERCENT + + // Calculate where the scroll should end + val targetY = displayHeight - amountInPx - + touchSlopPx - + safetyMarginPx - + INSETS_SIZE + + assertEquals(displayWidth / 2.0, upEvent.x.toDouble(), 0.0) + assertEquals(targetY, upEvent.y, 0.0f) + } + + @Test + fun `should scroll left to edge on full screen view when gesture navigation enabled`() { + ScrollHelper.performOnce(uiControllerMock, viewMock, MOTION_DIR_LEFT) + val upEvent = getUpEvent() + val amountInPx = displayWidth * SCROLL_RANGE_SAFE_PERCENT + + // Calculate where the scroll should end + val targetX = amountInPx + + touchSlopPx + + INSETS_SIZE + + assertEquals(targetX, upEvent.x, 0.0f) + assertEquals(displayHeight / 2.0, upEvent.y.toDouble(), 0.0) + } + + @Test + fun `should scroll up to edge on full screen view when gesture navigation enabled`() { + ScrollHelper.performOnce(uiControllerMock, viewMock, MOTION_DIR_UP) + val upEvent = getUpEvent() + val amountInPx = displayHeight * SCROLL_RANGE_SAFE_PERCENT + + // Calculate where the scroll should end + val targetY = amountInPx + + touchSlopPx + + INSETS_SIZE assertEquals(displayWidth / 2.0, upEvent.x.toDouble(), 0.0) - assertEquals(displayHeight - amountInPx - touchSlopPx - safetyMarginPx - INSETS_SIZE, upEvent.y, 0.0f) + assertEquals(targetY, upEvent.y, 0.0f) + } + + @Test + fun `should scroll right to edge on full screen view when gesture navigation enabled`() { + ScrollHelper.performOnce(uiControllerMock, viewMock, MOTION_DIR_RIGHT) + val upEvent = getUpEvent() + val amountInPx = displayWidth * SCROLL_RANGE_SAFE_PERCENT + // Calculate where the scroll should end + val targetX = displayWidth - amountInPx - + touchSlopPx - + safetyMarginPx - + INSETS_SIZE + + assertEquals(targetX, upEvent.x, 0.0f) + assertEquals(displayHeight / 2.0, upEvent.y.toDouble(), 0.0) } /** @@ -73,6 +145,17 @@ class ScrollHelperTest { return listOfCapturedEvents.last() } + private fun mockViewWithoutGestureNavigation(displayWidth: Int, displayHeight: Int): View { + // This is how we disable gesture navigation + val windowInsets = mock() { + whenever(it.systemGestureInsets).thenReturn( + Insets.of(0, 0, 0, 0) + ) + } + + return mockView(displayWidth, displayHeight, windowInsets) + } + /** * Mock a view with gesture navigation enabled */ @@ -84,10 +167,19 @@ class ScrollHelperTest { ) } + return mockView(displayWidth, displayHeight, windowInsets) + } + + private fun mockView( + displayWidth: Int, + displayHeight: Int, + windowInsets: WindowInsets + ): View { val view = mock() { whenever(it.width).thenReturn(displayWidth) whenever(it.height).thenReturn(displayHeight) whenever(it.canScrollVertically(any())).thenReturn(true) // We allow endless scroll + whenever(it.canScrollHorizontally(any())).thenReturn(true) // We allow endless scroll whenever(it.context).thenReturn(InstrumentationRegistry.getInstrumentation().targetContext) whenever(it.rootWindowInsets).thenReturn(windowInsets) } From db80ee747e3b547d2329d54fce46a845b4820050 Mon Sep 17 00:00:00 2001 From: Georgy Steshin Date: Wed, 10 Jan 2024 08:37:17 +0200 Subject: [PATCH 5/5] Fixed tests after merge --- .../com/wix/detox/espresso/scroll/ScrollHelperTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/ScrollHelperTest.kt b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/ScrollHelperTest.kt index 778781313b..4eeee3a823 100644 --- a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/ScrollHelperTest.kt +++ b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/scroll/ScrollHelperTest.kt @@ -72,7 +72,7 @@ class ScrollHelperTest { @Test fun `should scroll down to edge on full screen view when gesture navigation enabled`() { - ScrollHelper.performOnce(uiControllerMock, viewMock, MOTION_DIR_DOWN) + ScrollHelper.performOnce(uiControllerMock, viewMock, MOTION_DIR_DOWN, null, null) val upEvent = getUpEvent() val amountInPx = displayHeight * SCROLL_RANGE_SAFE_PERCENT @@ -88,7 +88,7 @@ class ScrollHelperTest { @Test fun `should scroll left to edge on full screen view when gesture navigation enabled`() { - ScrollHelper.performOnce(uiControllerMock, viewMock, MOTION_DIR_LEFT) + ScrollHelper.performOnce(uiControllerMock, viewMock, MOTION_DIR_LEFT, null, null) val upEvent = getUpEvent() val amountInPx = displayWidth * SCROLL_RANGE_SAFE_PERCENT @@ -103,7 +103,7 @@ class ScrollHelperTest { @Test fun `should scroll up to edge on full screen view when gesture navigation enabled`() { - ScrollHelper.performOnce(uiControllerMock, viewMock, MOTION_DIR_UP) + ScrollHelper.performOnce(uiControllerMock, viewMock, MOTION_DIR_UP,null, null) val upEvent = getUpEvent() val amountInPx = displayHeight * SCROLL_RANGE_SAFE_PERCENT @@ -118,7 +118,7 @@ class ScrollHelperTest { @Test fun `should scroll right to edge on full screen view when gesture navigation enabled`() { - ScrollHelper.performOnce(uiControllerMock, viewMock, MOTION_DIR_RIGHT) + ScrollHelper.performOnce(uiControllerMock, viewMock, MOTION_DIR_RIGHT, null, null) val upEvent = getUpEvent() val amountInPx = displayWidth * SCROLL_RANGE_SAFE_PERCENT