Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve scroll speed and add scrollToIndex #2854

Merged
merged 9 commits into from
Jul 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.wix.detox.espresso.action.DetoxMultiTap;
import com.wix.detox.espresso.action.RNClickAction;
import com.wix.detox.espresso.action.ScreenshotResult;
import com.wix.detox.espresso.action.ScrollToIndexAction;
import com.wix.detox.espresso.action.TakeViewScreenshotAction;
import com.wix.detox.espresso.action.GetAttributesAction;
import com.wix.detox.action.common.MotionDir;
Expand Down Expand Up @@ -97,8 +98,8 @@ public void perform(UiController uiController, View view) {
/**
* Scrolls the View in a direction by the Density Independent Pixel amount.
*
* @param direction Direction to scroll (see {@link MotionDir})
* @param amountInDP Density Independent Pixels
* @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.
*/
Expand All @@ -114,8 +115,8 @@ public static ViewAction scrollInDirection(final int direction, final double amo
* where the scrolling-edge is reached, by throwing the {@link StaleActionException} exception (i.e.
* so as to make this use case manageable by the user).
*
* @param direction Direction to scroll (see {@link MotionDir})
* @param amountInDP Density Independent Pixels
* @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.
*/
Expand All @@ -128,9 +129,9 @@ public static ViewAction scrollInDirectionStaleAtEdge(final int direction, final
/**
* Swipes the View in a direction.
*
* @param direction Direction to swipe (see {@link MotionDir})
* @param fast true if fast, false if slow
* @param normalizedOffset or "swipe amount" between 0.0 and 1.0, relative to the screen width/height
* @param direction Direction to swipe (see {@link MotionDir})
* @param fast true if fast, false if slow
* @param normalizedOffset or "swipe amount" between 0.0 and 1.0, relative to the screen width/height
* @param normalizedStartingPointX X coordinate of swipe starting point (between 0.0 and 1.0), relative to the view width
* @param normalizedStartingPointY Y coordinate of swipe starting point (between 0.0 and 1.0), relative to the view height
*/
Expand All @@ -143,6 +144,10 @@ public static ViewAction getAttributes() {
return new GetAttributesAction();
}

public static ViewAction scrollToIndex(int index) {
return new ScrollToIndexAction(index);
}

public static ViewAction takeViewScreenshot() {
return new ViewActionWithResult<String>() {
private final TakeViewScreenshotAction action = new TakeViewScreenshotAction();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package com.wix.detox.espresso.action

import android.view.View
import android.view.ViewGroup
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.matcher.ViewMatchers
import com.facebook.react.views.scroll.ReactHorizontalScrollView
import com.facebook.react.views.scroll.ReactScrollView
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.scroll.ScrollEdgeException
import com.wix.detox.espresso.scroll.ScrollHelper
import org.hamcrest.Matcher
import org.hamcrest.Matchers

class ScrollToIndexAction(private val index: Int) : ViewAction {
override fun getConstraints(): Matcher<View> {
return Matchers.anyOf(
Matchers.allOf(
ViewMatchers.isAssignableFrom(
View::class.java
), Matchers.instanceOf(
ReactScrollView::class.java
)
),
Matchers.allOf(
ViewMatchers.isAssignableFrom(
View::class.java
), Matchers.instanceOf(ReactHorizontalScrollView::class.java)
)
)
}

override fun getDescription(): String {
return "scrollToIndex"
}

override fun perform(uiController: UiController?, view: View?) {
if (index < 0) return

val offsetPercent = 0.4f
val reactScrollView = view as ViewGroup
val internalContainer = reactScrollView.getChildAt(0) as ViewGroup
val childCount = internalContainer.childCount
if (index >= childCount) return

val isHorizontalScrollView = getIsHorizontalScrollView(reactScrollView)
val targetPosition = getTargetPosition(isHorizontalScrollView, internalContainer, index)
var currentPosition = getCurrentPosition(isHorizontalScrollView, reactScrollView)
val jumpSize = getTargetDimension(isHorizontalScrollView, internalContainer, index)
val scrollDirection =
getScrollDirection(isHorizontalScrollView, currentPosition, targetPosition)

// either we'll find the target view or we'll hit the edge of the scrollview

// either we'll find the target view or we'll hit the edge of the scrollview
while (true) {
if (Math.abs(currentPosition - targetPosition) < jumpSize) {
// we found the target view
return
}
currentPosition = try {
ScrollHelper.perform(
uiController,
view,
scrollDirection,
jumpSize.toDouble(),
offsetPercent,
offsetPercent
)
getCurrentPosition(isHorizontalScrollView, reactScrollView)
} catch (e: ScrollEdgeException) {
// we hit the edge
return
}
}
}

}

private fun getScrollDirection(
isHorizontalScrollView: Boolean,
currentPosition: Int,
targetPosition: Int
): Int {
return if (isHorizontalScrollView) {
if (currentPosition < targetPosition) MOTION_DIR_RIGHT else MOTION_DIR_LEFT
} else {
if (currentPosition < targetPosition) MOTION_DIR_DOWN else MOTION_DIR_UP
}
}

private fun getIsHorizontalScrollView(scrollView: ViewGroup): Boolean {
return scrollView.canScrollHorizontally(1) || scrollView.canScrollHorizontally(-1)
}

private fun getCurrentPosition(isHorizontalScrollView: Boolean, scrollView: ViewGroup): Int {
return if (isHorizontalScrollView) scrollView.scrollX else scrollView.scrollY
}

private fun getTargetDimension(
isHorizontalScrollView: Boolean,
internalContainer: ViewGroup,
index: Int
): Int {
return if (isHorizontalScrollView) internalContainer.getChildAt(index).measuredWidth else internalContainer.getChildAt(
index
).measuredHeight
}

private fun getTargetPosition(
isHorizontalScrollView: Boolean,
internalContainer: ViewGroup,
index: Int
): Int {
var necessaryTarget = 0
for (childIndex in 0 until index) {
necessaryTarget += if (isHorizontalScrollView) internalContainer.getChildAt(childIndex).measuredWidth else internalContainer.getChildAt(
childIndex
).measuredHeight
}
return necessaryTarget
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class FlinglessSwiper @JvmOverloads constructor(

companion object {
// private const val LOG_TAG = "DetoxBatchedSwiper"
private const val VELOCITY_SAFETY_RATIO = .85f
private const val VELOCITY_SAFETY_RATIO = .99f
private const val FAST_EVENTS_RATIO = .75f
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ object FlinglessSwiperSpec: Spek({
}

describe("move") {
val SWIPER_VELOCITY = 85f
val SWIPER_VELOCITY = 99f

beforeEachTest {
whenever(viewConfig.scaledMinimumFlingVelocity).doReturn(100) // i.e. we expect the swiper to apply a safety margin of 85%, hence actual velocity = 85 px/sec
whenever(viewConfig.scaledMinimumFlingVelocity).doReturn(100) // i.e. we expect the swiper to apply a safety margin of 99%, hence actual velocity = 99 px/sec
}

it("should obtain a move event") {
Expand Down Expand Up @@ -121,7 +121,7 @@ object FlinglessSwiperSpec: Spek({

with(uut()) {
startAt(0f, 0f)
moveTo(0f, 42.5f)
moveTo(0f, SWIPER_VELOCITY/2)
}

verify(motionEvents).obtainMoveEvent(any(), eq(expectedEventTime), any(), any())
Expand Down Expand Up @@ -162,7 +162,7 @@ object FlinglessSwiperSpec: Spek({
}

verify(motionEvents, times(3)).obtainMoveEvent(any(), eq(swipeStartTime + 10L), any(), any())
verify(motionEvents, times(1)).obtainMoveEvent(any(), eq(swipeStartTime + 1000L), any(), any())
verify(motionEvents, times(1)).obtainMoveEvent(any(), eq(swipeStartTime + 858), any(), any())
}
}
}
Expand Down Expand Up @@ -190,9 +190,9 @@ object FlinglessSwiperSpec: Spek({

with(uut()) {
startAt(666f, 999f)
finishAt(666f + 85f, 999f + 85f)
finishAt(666f + 99f, 999f + 99f)
}
verify(motionEvents).obtainUpEvent(downEvent, expectedEventTime, 666f + 85f, 999f + 85f)
verify(motionEvents).obtainUpEvent(downEvent, expectedEventTime, 666f + 99f, 999f + 99f)
}

it("should finish by flushing all events to ui controller") {
Expand Down
8 changes: 8 additions & 0 deletions detox/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,14 @@ declare global {
startPositionY?: number,
): Promise<void>;

/**
* Scroll to index.
* @example await element(by.id('scrollView')).scrollToIndex(10);
*/
scrollToIndex(
index: Number
): Promise<void>;

/**
* Scroll to edge.
* @example await element(by.id('scrollView')).scrollTo('bottom');
Expand Down
1 change: 1 addition & 0 deletions detox/src/android/AndroidExpect.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ describe('AndroidExpect', () => {
await e.element(e.by.id('ScrollView161')).scrollTo('top');
await e.element(e.by.id('ScrollView161')).scrollTo('left');
await e.element(e.by.id('ScrollView161')).scrollTo('right');
await e.element(e.by.id('ScrollView161')).scrollToIndex(0);
});

it('should not scroll given bad args', async () => {
Expand Down
8 changes: 8 additions & 0 deletions detox/src/android/actions/native.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ class GetAttributes extends Action {
}
}

class ScrollToIndex extends Action {
constructor(index) {
super();
this._call = invoke.callDirectly(DetoxActionApi.scrollToIndex(index));
}
}

class TakeElementScreenshot extends Action {
constructor() {
super();
Expand All @@ -140,4 +147,5 @@ module.exports = {
ScrollEdgeAction,
SwipeAction,
TakeElementScreenshot,
ScrollToIndex,
};
5 changes: 5 additions & 0 deletions detox/src/android/core/NativeElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ class NativeElement {
return await new ActionInteraction(this._invocationManager, this, new actions.ScrollEdgeAction(edge)).execute();
}

async scrollToIndex(index) {
this._selectElementWithMatcher(this._originalMatcher._extendToDescendantScrollViews());
return await new ActionInteraction(this._invocationManager, this, new actions.ScrollToIndex(index)).execute();
}

/**
* @param {'up' | 'right' | 'down' | 'left'} direction
* @param {'slow' | 'fast'} [speed]
Expand Down
15 changes: 15 additions & 0 deletions detox/src/android/espressoapi/DetoxAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,21 @@ class DetoxAction {
};
}

static scrollToIndex(index) {
if (typeof index !== "number") throw new Error("index should be a number, but got " + (index + (" (" + (typeof index + ")"))));
return {
target: {
type: "Class",
value: "com.wix.detox.espresso.DetoxAction"
},
method: "scrollToIndex",
args: [{
type: "Integer",
value: index
}]
};
}

static takeViewScreenshot() {
return {
target: {
Expand Down
32 changes: 32 additions & 0 deletions detox/test/e2e/03.actions-scroll.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,36 @@ describe('Actions - Scroll', () => {
await element(by.id('toggleScrollOverlays')).tap();
await expect(element(by.text('HText6'))).not.toBeVisible();
});

it(':android: should be able to scrollToIndex on horizontal scrollviews', async () => {
// should ignore out of bounds children
await element(by.id('ScrollViewH')).scrollToIndex(3000);
await element(by.id('ScrollViewH')).scrollToIndex(-1);
await expect(element(by.text('HText1'))).toBeVisible();

await expect(element(by.text('HText8'))).not.toBeVisible();
await element(by.id('ScrollViewH')).scrollToIndex(7);
await expect(element(by.text('HText8'))).toBeVisible();
await expect(element(by.text('HText1'))).not.toBeVisible();

await element(by.id('ScrollViewH')).scrollToIndex(0);
await expect(element(by.text('HText1'))).toBeVisible();
await expect(element(by.text('HText8'))).not.toBeVisible();
});

it(':android: should be able to scrollToIndex on vertical scrollviews', async () => {
// should ignore out of bounds children
await element(by.id('ScrollView161')).scrollToIndex(3000);
await element(by.id('ScrollView161')).scrollToIndex(-1);
await expect(element(by.text('Text1'))).toBeVisible();

await element(by.id('ScrollView161')).scrollToIndex(11);
await expect(element(by.text('Text12'))).toBeVisible();

await element(by.id('ScrollView161')).scrollToIndex(0);
await expect(element(by.text('Text1'))).toBeVisible();

await element(by.id('ScrollView161')).scrollToIndex(7);
await expect(element(by.text('Text8'))).toBeVisible();
});
});
12 changes: 11 additions & 1 deletion docs/APIRef.ActionsOnElement.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Use [expectations](APIRef.Expect.md) to verify element states.
- [`.longPressAndDrag()`](#longpressanddragduration-normalizedpositionx-normalizedpositiony-targetelement-normalizedtargetpositionx-normalizedtargetpositiony-speed-holdduration--ios-only) **iOS only**
- [`.swipe()`](#swipedirection-speed-normalizedoffset-normalizedstartingpointx-normalizedstartingpointy)
- [`.pinch()`](#pinchscale-speed-angle--ios-only) **iOS only**
- [`.scrollToIndex()`](#scrolltoindexindex--android-only) **Android only**
- [`.scroll()`](#scrolloffset-direction-startpositionx-startpositiony)
- [`whileElement()`](#whileelementelement)
- [`.scrollTo()`](#scrolltoedge)
Expand Down Expand Up @@ -106,6 +107,15 @@ await element(by.id('PinchableScrollView')).pinch(1.1); //Zooms in a little bit
await element(by.id('PinchableScrollView')).pinch(2.0); //Zooms in a lot
await element(by.id('PinchableScrollView')).pinch(0.001); //Zooms out a lot
```
### `scrollToIndex(index)` Android only

Scrolls until it reaches the element with the provided index. This works for ReactScrollView and ReactHorizontalScrollView.

`index`—the index of the target element <br/>

```js
await element(by.id('scrollView')).scrollToIndex(0);
```
### `scroll(offset, direction, startPositionX, startPositionY)`

Simulates a scroll on the element with the provided options.
Expand Down Expand Up @@ -318,4 +328,4 @@ Simulates a pinch on the element with the provided options.

```js
await element(by.id('PinchableScrollView')).pinchWithAngle('outward', 'slow', 0);
```
```