Repository: wix/Detox
Pull request #4309 (2023-12-22 11:45:26)

Add start position as an optional parameter to scrollTo action
## Description

<!--
Thank you for contributing!

Step 1: Before submitting a pull request that introduces a new functionality or fixes a bug,
please open an issue where we could discuss the suggestion, problem - and potential ways to fix.
-->

- This pull request addresses the issue described here: #3512 

<!--
Step 2: Provide an overview of how your fix / enhancement works.
If possible, provide screenshots of the before and after states (even for simple command line options - show the terminal).
-->

In this pull request, I have …

<!--
Step 3: Please review the checklist below.
-->

---


> _For features/enhancements:_
 - [X] I have added/updated the relevant references in the [documentation](https://github.com/wix/Detox/tree/master/docs) files.

> _For API changes:_
 - [X] I have made the necessary changes in the [types index](https://github.com/wix/Detox/blob/master/detox/index.d.ts) file.


asafkorem: Great work @andrey-wix! 👑 Huge thanks!!
I will start reviewing this today

Patch:
diff --git a/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxAction.java b/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxAction.java
index 8474e3526..8ffa60d06 100644
--- a/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxAction.java
+++ b/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxAction.java
@@ -79,9 +79,14 @@ public class DetoxAction {
      * Scrolls to the edge of the given scrollable view.
      *
      * @param edge Direction to scroll (see {@link MotionDir})
+     * @param startOffsetPercentX Percentage denoting where the scroll should start from on the X-axis, with respect to the scrollable view.
+     * @param startOffsetPercentY Percentage denoting where the scroll should start from on the Y-axis, with respect to the scrollable view.
      * @return ViewAction
      */
-    public static ViewAction scrollToEdge(final int edge) {
+    public static ViewAction scrollToEdge(final int edge, double startOffsetPercentX, double startOffsetPercentY) {
+        final Float _startOffsetPercentX = startOffsetPercentX < 0 ? null : (float) startOffsetPercentX;
+        final Float _startOffsetPercentY = startOffsetPercentY < 0 ? null : (float) startOffsetPercentY;
+
         return actionWithAssertions(new ViewAction() {
             @Override
             public Matcher<View> getConstraints() {
@@ -97,7 +102,7 @@ public class DetoxAction {
             public void perform(UiController uiController, View view) {
                 try {
                     for (int i = 0; i < 100; i++) {
-                        ScrollHelper.performOnce(uiController, view, edge);
+                        ScrollHelper.performOnce(uiController, view, edge, _startOffsetPercentX, _startOffsetPercentY);
                     }
                     throw new DetoxRuntimeException("Scrolling a lot without reaching the edge: force-breaking the loop");
                 } catch (ScrollEdgeException e) {
@@ -112,8 +117,8 @@ public class DetoxAction {
      *
      * @param direction           Direction to scroll (see {@link MotionDir})
      * @param amountInDP          Density Independent Pixels
-     * @param startOffsetPercentX Percentage denoting where X-swipe should start, with respect to the scrollable view.
-     * @param startOffsetPercentY Percentage denoting where Y-swipe should start, with respect to the scrollable view.
+     * @param startOffsetPercentX Percentage denoting where the scroll should start from on the X-axis, with respect to the scrollable view.
+     * @param startOffsetPercentY Percentage denoting where the scroll should start from on the Y-axis, with respect to the scrollable view.
      */
     public static ViewAction scrollInDirection(final int direction, final double amountInDP, double startOffsetPercentX, double startOffsetPercentY) {
         final Float _startOffsetPercentX = startOffsetPercentX < 0 ? null : (float) startOffsetPercentX;
@@ -129,8 +134,8 @@ public class DetoxAction {
      *
      * @param direction           Direction to scroll (see {@link MotionDir})
      * @param amountInDP          Density Independent Pixels
-     * @param startOffsetPercentX Percentage denoting where X-swipe should start, with respect to the scrollable view.
-     * @param startOffsetPercentY Percentage denoting where Y-swipe should start, with respect to the scrollable view.
+     * @param startOffsetPercentX Percentage denoting where the scroll should start from on the X-axis, with respect to the scrollable view.
+     * @param startOffsetPercentY Percentage denoting where the scroll should start from on the Y-axis, with respect to the scrollable view.
      */
     public static ViewAction scrollInDirectionStaleAtEdge(final int direction, final double amountInDP, double startOffsetPercentX, double startOffsetPercentY) {
         final Float _startOffsetPercentX = startOffsetPercentX < 0 ? null : (float) startOffsetPercentX;
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 bfca25cb1..ac0810084 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
@@ -42,8 +42,8 @@ public class ScrollHelper {
      *
      * @param direction Direction to scroll (see {@link MotionDir})
      * @param amountInDP Density Independent Pixels
-     * @param startOffsetPercentX Percentage denoting where X-swipe should start, with respect to the scrollable view. Null means select automatically.
-     * @param startOffsetPercentY Percentage denoting where Y-swipe should start, with respect to the scrollable view. Null means select automatically.
+     * @param startOffsetPercentX Percentage denoting where the scroll should start from on the X-axis, with respect to the scrollable view. Null means select automatically.
+     * @param startOffsetPercentY Percentage denoting where the scroll should start from on the Y-axis, with respect to the scrollable view. Null means select automatically.
      */
     public static void perform(UiController uiController, View view, @MotionDir int direction, double amountInDP, Float startOffsetPercentX, Float startOffsetPercentY) throws ScrollEdgeException {
         final int amountInPx = DeviceDisplay.convertDpiToPx(amountInDP);
@@ -64,10 +64,12 @@ public class ScrollHelper {
      * of the screen.)
      *
      * @param direction Direction to scroll (see {@link @MotionDir})
+     * @param startOffsetPercentX Percentage denoting where the scroll should start from on the X-axis, with respect to the scrollable view. Null means select automatically.
+     * @param startOffsetPercentY Percentage denoting where the scroll should start from on the Y-axis, with respect to the scrollable view. Null means select automatically.
      */
-    public static void performOnce(UiController uiController, View view, @MotionDir int direction) throws ScrollEdgeException {
+    public static void performOnce(UiController uiController, View view, @MotionDir int direction, Float startOffsetPercentX, Float startOffsetPercentY) throws ScrollEdgeException {
         final int scrollableRangePx = getViewSafeScrollableRangePix(view, direction);
-        scrollOnce(uiController, view, direction, scrollableRangePx, null, null);
+        scrollOnce(uiController, view, direction, scrollableRangePx, startOffsetPercentX, startOffsetPercentY);
     }
 
     private static void scrollOnce(UiController uiController, View view, @MotionDir int direction, int userAmountPx, Float startOffsetPercentX, Float startOffsetPercentY) throws ScrollEdgeException {
diff --git a/detox/detox.d.ts b/detox/detox.d.ts
index 86314cccd..b39ae586c 100644
--- a/detox/detox.d.ts
+++ b/detox/detox.d.ts
@@ -1363,10 +1363,13 @@ declare global {
 
             /**
              * Scroll to edge.
-             * @example await element(by.id('scrollView')).scrollTo('bottom');
+             * @param  edge - left|right|top|bottom
+             * @param startPositionX - the X starting scroll position, in percentage; valid input: `[0.0, 1.0]`, `NaN`; default: `NaN`—choose the best value automatically
+             * @param startPositionY - the Y starting scroll position, in percentage; valid input: `[0.0, 1.0]`, `NaN`; default: `NaN`—choose the best value automatically
+             * @example await element(by.id('scrollView')).scrollTo('bottom', NaN, 0.85);
              * @example await element(by.id('scrollView')).scrollTo('top');
              */
-            scrollTo(edge: Direction): Promise<void>;
+            scrollTo(edge: Direction, startPositionX?: number, startPositionY?: number): Promise<void>;
 
             /**
              * Adjust slider to position.
diff --git a/detox/ios/Detox/Actions/UIScrollView+DetoxActions.h b/detox/ios/Detox/Actions/UIScrollView+DetoxActions.h
index c2c0638fd..5719a0ca5 100644
--- a/detox/ios/Detox/Actions/UIScrollView+DetoxActions.h
+++ b/detox/ios/Detox/Actions/UIScrollView+DetoxActions.h
@@ -13,6 +13,8 @@ NS_ASSUME_NONNULL_BEGIN
 @interface UIScrollView (DetoxActions)
 
 - (void)dtx_scrollToEdge:(UIRectEdge)edge NS_SWIFT_NAME(dtx_scroll(to:));
+- (void)dtx_scrollToEdge:(UIRectEdge)edge
+ normalizedStartingPoint:(CGPoint)normalizedStartingPoint;
 - (void)dtx_scrollWithOffset:(CGPoint)offset;
 - (void)dtx_scrollWithOffset:(CGPoint)offset normalizedStartingPoint:(CGPoint)normalizedStartingPoint NS_SWIFT_NAME(dtx_scroll(withOffset:normalizedStartingPoint:));
 
diff --git a/detox/ios/Detox/Actions/UIScrollView+DetoxActions.m b/detox/ios/Detox/Actions/UIScrollView+DetoxActions.m
index 5bcd14747..d78c470e4 100644
--- a/detox/ios/Detox/Actions/UIScrollView+DetoxActions.m
+++ b/detox/ios/Detox/Actions/UIScrollView+DetoxActions.m
@@ -83,7 +83,7 @@ else \
 [self setContentOffset:pointMakeMacro(target) animated:YES]; \
 [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:[[self valueForKeyPath:@"animation.duration"] doubleValue] + 0.05]];
 
-- (void)dtx_scrollToEdge:(UIRectEdge)edge
+- (CGPoint)_edgeToNormalizedEdge:(UIRectEdge)edge
 {
 	CGPoint normalizedEdge;
 	switch (edge) {
@@ -100,10 +100,19 @@ else \
 			normalizedEdge = CGPointMake(1, 0);
 			break;
 		default:
+			normalizedEdge= CGPointMake(0, 0);
 			DTXAssert(NO, @"Incorect edge provided.");
-			return;
 	}
-	
+	return normalizedEdge;
+}
+
+
+- (void)dtx_scrollToEdge:(UIRectEdge)edge
+{
+	CGPoint normalizedEdge = [self _edgeToNormalizedEdge:edge];
+	if(normalizedEdge.x == 0 && normalizedEdge.y == 0)
+		return;
+
 	[self _dtx_scrollToNormalizedEdge:normalizedEdge];
 }
 
@@ -121,6 +130,23 @@ else \
 	[self _dtx_scrollWithOffset:CGPointMake(- edge.x * CGFLOAT_MAX, - edge.y * CGFLOAT_MAX) normalizedStartingPoint:CGPointMake(NAN, NAN) strict:NO];
 }
 
+- (void)dtx_scrollToEdge:(UIRectEdge)edge
+	normalizedStartingPoint:(CGPoint)normalizedStartingPoint
+{
+	CGPoint normalizedEdge = [self _edgeToNormalizedEdge:edge];
+	if(normalizedEdge.x == 0 && normalizedEdge.y == 0)
+		return;
+	
+	[self _dtx_scrollToNormalizedEdge:normalizedEdge normalizedStartingPoint: normalizedStartingPoint ];
+}
+
+- (void)_dtx_scrollToNormalizedEdge:(CGPoint)edge
+			normalizedStartingPoint:(CGPoint)normalizedStartingPoint
+{
+	[self _dtx_scrollWithOffset:CGPointMake(- edge.x * CGFLOAT_MAX, - edge.y * CGFLOAT_MAX) normalizedStartingPoint:normalizedStartingPoint strict:NO];
+}
+
+
 DTX_ALWAYS_INLINE
 static NSString* _DTXScrollDirectionDescriptionWithOffset(CGPoint offset)
 {
diff --git a/detox/ios/Detox/Invocation/Action.swift b/detox/ios/Detox/Invocation/Action.swift
index a4ac09db3..1a4470d74 100644
--- a/detox/ios/Detox/Invocation/Action.swift
+++ b/detox/ios/Detox/Invocation/Action.swift
@@ -129,6 +129,15 @@ class Action : CustomStringConvertible {
 		}
 	}
 	
+	func startPosition(forIndex index: Int, in params: [Any]?) -> Double {
+		guard params?.count ?? 0 > index,
+			  let param = params?[index] as? Double,
+			  param.isNaN == false else {
+			return Double.nan
+		}
+		return param
+	}
+	
 	var description: String {
 		let paramsDescription: String
 		if let params = params {
@@ -434,9 +443,13 @@ class ScrollToEdgeAction : Action {
 			fatalError("Unknown scroll direction")
 			break;
 		}
-		
-		element.scroll(to: targetEdge)
-		
+
+		let startPositionX = startPosition(forIndex: 1, in: params)
+		let startPositionY = startPosition(forIndex: 2, in: params)
+		let normalizedStartingPoint = CGPoint(x: startPositionX, y: startPositionY)
+
+		element.scroll(to: targetEdge, normalizedStartingPoint: normalizedStartingPoint)
+
 		return nil
 	}
 }
@@ -487,18 +500,9 @@ class SwipeAction : Action {
 		targetNormalizedOffset.x *= CGFloat(appliedPercentage)
 		targetNormalizedOffset.y *= CGFloat(appliedPercentage)
 		
-		let startPositionX : Double
-		if params?.count ?? 0 > 3, let param2 = params?[3] as? Double, param2.isNaN == false {
-			startPositionX = param2
-		} else {
-			startPositionX = Double.nan
-		}
-		let startPositionY : Double
-		if params?.count ?? 0 > 4, let param3 = params?[4] as? Double, param3.isNaN == false {
-			startPositionY = param3
-		} else {
-			startPositionY = Double.nan
-		}
+		
+		let startPositionX = startPosition(forIndex: 3, in: params)
+		let startPositionY = startPosition(forIndex: 4, in: params)
 		let normalizedStartingPoint = CGPoint(x: startPositionX, y: startPositionY)
 		
 		element.swipe(normalizedOffset: targetNormalizedOffset, velocity: velocity, normalizedStartingPoint: normalizedStartingPoint)
diff --git a/detox/ios/Detox/Invocation/Element.swift b/detox/ios/Detox/Invocation/Element.swift
index 5570ab5ba..378608b8a 100644
--- a/detox/ios/Detox/Invocation/Element.swift
+++ b/detox/ios/Detox/Invocation/Element.swift
@@ -140,10 +140,13 @@ class Element : NSObject {
 		view.dtx_pinch(withScale: scale, velocity: velocity, angle: angle)
 	}
 	
-	func scroll(to edge: UIRectEdge) {
+	func scroll(to edge: UIRectEdge, normalizedStartingPoint: CGPoint? = nil) {
 		let scrollView = extractScrollView()
-		
-		scrollView.dtx_scroll(to: edge)
+		if let normalizedStartingPoint = normalizedStartingPoint {
+			scrollView.dtx_scroll(to: edge, normalizedStarting: normalizedStartingPoint)
+		} else {
+			scrollView.dtx_scroll(to: edge)
+		}
 	}
 
 	func scroll(withOffset offset: CGPoint, normalizedStartingPoint: CGPoint? = nil) {
diff --git a/detox/src/android/actions/native.js b/detox/src/android/actions/native.js
index 601bef562..7b28efbed 100644
--- a/detox/src/android/actions/native.js
+++ b/detox/src/android/actions/native.js
@@ -81,10 +81,10 @@ class ScrollAmountStopAtEdgeAction extends Action {
 }
 
 class ScrollEdgeAction extends Action {
-  constructor(edge) {
+  constructor(edge, startPositionX = -1, startPositionY = -1) {
     super();
 
-    this._call = invoke.callDirectly(DetoxActionApi.scrollToEdge(edge));
+    this._call = invoke.callDirectly(DetoxActionApi.scrollToEdge(edge, startPositionX, startPositionY));
   }
 }
 
diff --git a/detox/src/android/core/NativeElement.js b/detox/src/android/core/NativeElement.js
index 1ecbfa759..d493a83e8 100644
--- a/detox/src/android/core/NativeElement.js
+++ b/detox/src/android/core/NativeElement.js
@@ -93,12 +93,12 @@ class NativeElement {
     return await new ActionInteraction(this._invocationManager, this._matcher, action, traceDescription).execute();
   }
 
-  async scrollTo(edge) {
+  async scrollTo(edge, startPositionX, startPositionY) {
     // override the user's element selection with an extended matcher that looks for UIScrollView children
     this._matcher = this._matcher._extendToDescendantScrollViews();
 
-    const action = new actions.ScrollEdgeAction(edge);
-    const traceDescription = actionDescription.scrollTo(edge);
+    const action = new actions.ScrollEdgeAction(edge, startPositionX, startPositionY);
+    const traceDescription = actionDescription.scrollTo(edge, startPositionX, startPositionY);
     return await new ActionInteraction(this._invocationManager, this._matcher, action, traceDescription).execute();
   }
 
diff --git a/detox/src/android/espressoapi/DetoxAction.js b/detox/src/android/espressoapi/DetoxAction.js
index 6ed2d213f..df5c31390 100644
--- a/detox/src/android/espressoapi/DetoxAction.js
+++ b/detox/src/android/espressoapi/DetoxAction.js
@@ -68,8 +68,10 @@ class DetoxAction {
     };
   }
 
-  static scrollToEdge(edge) {
+  static scrollToEdge(edge, startOffsetPercentX, startOffsetPercentY) {
     if (typeof edge !== "string") throw new Error("edge should be a string, but got " + (edge + (" (" + (typeof edge + ")"))));
+    if (typeof startOffsetPercentX !== "number") throw new Error("startOffsetPercentX should be a number, but got " + (startOffsetPercentX + (" (" + (typeof startOffsetPercentX + ")"))));
+    if (typeof startOffsetPercentY !== "number") throw new Error("startOffsetPercentY should be a number, but got " + (startOffsetPercentY + (" (" + (typeof startOffsetPercentY + ")"))));
     return {
       target: {
         type: "Class",
@@ -79,6 +81,12 @@ class DetoxAction {
       args: [{
         type: "Integer",
         value: sanitize_android_edge(edge)
+      }, {
+        type: "Double",
+        value: startOffsetPercentX
+      }, {
+        type: "Double",
+        value: startOffsetPercentY
       }]
     };
   }
diff --git a/detox/src/ios/expectTwo.js b/detox/src/ios/expectTwo.js
index b2153b438..f89b7f26a 100644
--- a/detox/src/ios/expectTwo.js
+++ b/detox/src/ios/expectTwo.js
@@ -247,10 +247,13 @@ class Element {
     return this.withAction('scroll', traceDescription, pixels, direction, startPositionX, startPositionY);
   }
 
-  scrollTo(edge) {
+  scrollTo(edge, startPositionX = NaN, startPositionY = NaN) {
     if (!['left', 'right', 'top', 'bottom'].some(option => option === edge)) throw new Error('edge should be one of [left, right, top, bottom], but got ' + edge);
-    const traceDescription = actionDescription.scrollTo(edge);
-    return this.withAction('scrollTo', traceDescription, edge);
+    if (typeof startPositionX !== 'number') throw new Error('startPositionX should be a number, but got ' + (startPositionX + (' (' + (typeof startPositionX + ')'))));
+    if (typeof startPositionY !== 'number') throw new Error('startPositionY should be a number, but got ' + (startPositionY + (' (' + (typeof startPositionY + ')'))));
+
+    const traceDescription = actionDescription.scrollTo(edge, startPositionX, startPositionY);
+    return this.withAction('scrollTo', traceDescription, edge, startPositionX, startPositionY);
   }
 
   swipe(direction, speed = 'fast', normalizedSwipeOffset = NaN, normalizedStartingPointX = NaN, normalizedStartingPointY = NaN) {
diff --git a/detox/src/utils/invocationTraceDescriptions.js b/detox/src/utils/invocationTraceDescriptions.js
index a9168327a..63969bf63 100644
--- a/detox/src/utils/invocationTraceDescriptions.js
+++ b/detox/src/utils/invocationTraceDescriptions.js
@@ -12,8 +12,9 @@ module.exports = {
     pinchWithAngle: (direction, speed, angle) => `pinch with direction ${direction}, speed ${speed}, and angle ${angle}`,
     replaceText: (value) => `replace input text: "${value}"`,
     scroll: (amount, direction, startPositionX, startPositionY) =>
-      `scroll ${amount} pixels ${direction}${startPositionX !== undefined && startPositionY !== undefined ? ` from normalized position (${startPositionX}, ${startPositionY})` : ''}`,
-    scrollTo: (edge) => `scroll to ${edge}`,
+      `scroll ${amount} pixels ${direction}${startPositionX !== undefined || startPositionY !== undefined ? ` from normalized position (${startPositionX}, ${startPositionY})` : ''}`,
+    scrollTo: (edge, startPositionX, startPositionY) =>
+      `scroll to ${edge} ${startPositionX !== undefined || startPositionY !== undefined ? ` from normalized position (${startPositionX}, ${startPositionY})` : ''}`,
     scrollToIndex: (index) => `scroll to index #${index}`,
     setColumnToValue: (column, value) => `set column ${column} to value ${value}`,
     setDatePickerDate: (dateString, dateFormat) => `set date picker date to ${dateString} using format ${dateFormat}`,
diff --git a/docs/api/actions.md b/docs/api/actions.md
index c249c0aa6..4c6d9f289 100644
--- a/docs/api/actions.md
+++ b/docs/api/actions.md
@@ -144,15 +144,17 @@ Continuously scrolls the scroll element until the specified expectation is resol
 await waitFor(element(by.text('Text5'))).toBeVisible().whileElement(by.id('ScrollView630')).scroll(50, 'down');
 ```
 
-### `scrollTo(edge)`
+### `scrollTo(edge[, startPositionX, startPositionY])`
 
 Simulates a scroll to the specified edge.
 
-`edge`—the edge to scroll to (valid input: `"left"`/`"right"`/`"top"`/`"bottom"`)
+`edge`—the edge to scroll to (valid input: `"left"`/`"right"`/`"top"`/`"bottom"`) <br/>
+`startPositionX`—the normalized x percentage of the element to use as scroll start point (optional, valid input: \[0.0, 1.0], `NaN`—choose an optimal value automatically, default is `NaN`) <br/>
+`startPositionY`—the normalized y percentage of the element to use as scroll start point (optional, valid input: \[0.0, 1.0], `NaN`—choose an optimal value automatically, default is `NaN`)
 
 ```js
 await element(by.id('scrollView')).scrollTo('bottom');
-await element(by.id('scrollView')).scrollTo('top');
+await element(by.id('scrollView')).scrollTo('top', NaN, 0.2);
 ```
 
 ### `typeText(text)`


Test patch:
diff --git a/detox/src/ios/expectTwoApiCoverage.test.js b/detox/src/ios/expectTwoApiCoverage.test.js
index 2980bbdb4..442366d75 100644
--- a/detox/src/ios/expectTwoApiCoverage.test.js
+++ b/detox/src/ios/expectTwoApiCoverage.test.js
@@ -181,6 +181,8 @@ describe('expectTwo API Coverage', () => {
 
       await expectToThrow(() => e.element(e.by.id('someId')).scrollTo(0));
       await expectToThrow(() => e.element(e.by.id('someId')).scrollTo('noDirection'));
+      await expectToThrow(() => e.element(e.by.id('someId')).scrollTo('top','Nan', 0.5));
+      await expectToThrow(() => e.element(e.by.id('someId')).scrollTo('top', 0.5, 'Nan'));
 
       await expectToThrow(() => e.element(e.by.id('someId')).swipe(4, 'fast'));
       await expectToThrow(() => e.element(e.by.id('someId')).swipe('left', 'fast', 20));
@@ -266,6 +268,7 @@ describe('expectTwo API Coverage', () => {
       await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).scroll(50, 'down');
       await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).scroll(50, 'down', 0, 0);
       await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).scrollTo('left');
+      await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).scrollTo('left', 0.1, 0.1);
       await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).swipe('left');
       await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).swipe('left', 'fast');
       await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).swipe('left', 'slow', 0.1);
diff --git a/detox/test/e2e/03.actions-scroll.test.js b/detox/test/e2e/03.actions-scroll.test.js
index e4cb32f44..6615d6ea4 100644
--- a/detox/test/e2e/03.actions-scroll.test.js
+++ b/detox/test/e2e/03.actions-scroll.test.js
@@ -46,6 +46,32 @@ describe('Actions - Scroll', () => {
     await expect(element(by.text('HText1'))).toBeVisible();
   });
 
+  it('should scroll to edge from a custom start-position ratio', async () => {
+    await expect(element(by.text('Text12'))).not.toBeVisible();
+    await element(by.id('toggleScrollOverlays')).tap();
+    await element(by.id('ScrollView161')).scrollTo('bottom', 0.2, 0.4);
+    await element(by.id('toggleScrollOverlays')).tap();
+    await expect(element(by.text('Text12'))).toBeVisible();
+
+    await element(by.id('toggleScrollOverlays')).tap();
+    await element(by.id('ScrollView161')).scrollTo('top', 0.8, 0.6);
+    await element(by.id('toggleScrollOverlays')).tap();
+    await expect(element(by.text('Text1'))).toBeVisible();
+  });
+
+  it('should scroll to edge horizontally from a custom start-position ratio', async () => {
+    await expect(element(by.text('HText8'))).not.toBeVisible();
+    await element(by.id('toggleScrollOverlays')).tap();
+    await element(by.id('ScrollViewH')).scrollTo('right', 0.8, 0.6);
+    await element(by.id('toggleScrollOverlays')).tap();
+    await expect(element(by.text('HText8'))).toBeVisible();
+
+    await element(by.id('toggleScrollOverlays')).tap();
+    await element(by.id('ScrollViewH')).scrollTo('left',0.2, 0.4);
+    await element(by.id('toggleScrollOverlays')).tap();
+    await expect(element(by.text('HText1'))).toBeVisible();
+  });
+
   it('should scroll from a custom start-position ratio', async () => {
     await expect(element(by.text('Text12'))).not.toBeVisible();
     await element(by.id('toggleScrollOverlays')).tap();