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

Issue/342 kautomator not working properly on views with several scrollables #344

Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.kaspersky.kaspresso.autoscroll

import androidx.test.espresso.ViewInteraction
import com.kaspersky.kaspresso.internal.extensions.other.isAllowed
import com.kaspersky.kaspresso.logger.UiTestLogger
import com.kaspersky.kaspresso.params.AutoScrollParams
import com.kaspersky.kaspresso.interceptors.behavior.impl.autoscroll.FallbackAutoScrollToAction

class AutoScrollFallbackProviderImpl(
private val params: AutoScrollParams,
private val logger: UiTestLogger
) : AutoScrollProvider<ViewInteraction> {

/**
* Invokes the given [action] and calls [scroll] if it fails. Helps in cases when autoscroll has already failed because of
* the scrollable container having paddings that autoscroll cannot successfully solve
*
* @param interaction the instance of [ViewInteraction] interface to perform actions and assertions.
* @param action the actual action on the interacted view.
*
* @throws Throwable if the exception caught while invoking [action] is not allowed via [params].
* @return the result of [action] invocation.
*/
@Throws(Throwable::class)
override fun <T> withAutoScroll(interaction: ViewInteraction, action: () -> T): T {
return try {
action.invoke()
} catch (error: Throwable) {
if (error.isAllowed(params.allowedExceptions)) {
return scroll(interaction, action, error)
}
throw error
}
}

/**
* Performs the autoscrolling functionality. Performs scroll and re-invokes the given [action].
*
* @param interaction the instance of [ViewInteraction] interface to perform actions and assertions.
* @param action the actual action on the interacted view.
* @param cachedError the error to be thrown if autoscroll would not help.
*
* @throws cachedError if autoscroll action did not help.
* @return the result of [action] invocation.
*/
@Throws(Throwable::class)
override fun <T> scroll(interaction: ViewInteraction, action: () -> T, cachedError: Throwable): T {
return try {
interaction.perform(FallbackAutoScrollToAction())
logger.i("View fallback autoScroll successfully performed.")
action.invoke()
} catch (error: Throwable) {
throw cachedError
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.kaspersky.kaspresso.interceptors.behavior.impl.autoscroll

import androidx.test.espresso.ViewInteraction
import com.kaspersky.kaspresso.autoscroll.AutoScrollFallbackProviderImpl
import com.kaspersky.kaspresso.autoscroll.AutoScrollProvider
import com.kaspersky.kaspresso.interceptors.behavior.ViewBehaviorInterceptor
import com.kaspersky.kaspresso.logger.UiTestLogger
import com.kaspersky.kaspresso.params.AutoScrollParams

class AutoScrollViewFallbackInterceptor(
params: AutoScrollParams,
logger: UiTestLogger
) : ViewBehaviorInterceptor,
AutoScrollProvider<ViewInteraction> by AutoScrollFallbackProviderImpl(params, logger) {

/**
* Wraps the given [action] invocation with the fallback autoscrolling on failure.
*
* @param interaction the intercepted [ViewInteraction].
* @param action the action to invoke.
*/
override fun <T> intercept(interaction: ViewInteraction, action: () -> T): T = withAutoScroll(interaction, action)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.kaspersky.kaspresso.interceptors.behavior.impl.autoscroll

import android.util.Log
import android.view.View
import android.view.ViewParent
import android.widget.HorizontalScrollView
import android.widget.ListView
import android.widget.ScrollView
import androidx.core.widget.NestedScrollView
import androidx.test.espresso.PerformException
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.util.HumanReadables
import org.hamcrest.Matcher
import org.hamcrest.Matchers

class FallbackAutoScrollToAction : ViewAction {

override fun getConstraints(): Matcher<View> {
return Matchers.allOf(
ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE),
ViewMatchers.isDescendantOfA(
Matchers.anyOf(
ViewMatchers.isAssignableFrom(NestedScrollView::class.java),
ViewMatchers.isAssignableFrom(ScrollView::class.java),
ViewMatchers.isAssignableFrom(HorizontalScrollView::class.java),
ViewMatchers.isAssignableFrom(ListView::class.java)
)
)
)
}

private fun View?.isScrollable(): Boolean =
(this is ScrollView || this is NestedScrollView || this is HorizontalScrollView || this is ListView)

private tailrec fun View?.findFirstParentScrollableView(lastView: View): View? {
return if (this == lastView) {
if (this.isScrollable()) this else null
} else {
if (this.isScrollable()) {
this
} else {
this?.parent?.findFirstView()?.findFirstParentScrollableView(lastView)
}
}
}

private tailrec fun ViewParent?.findFirstView(): View? {
return if (this is View) {
this
} else {
this?.parent.findFirstView()
}
}

override fun perform(uiController: UiController, view: View) {
if (ViewMatchers.isDisplayingAtLeast(FULLY_VISIBLE_PERCENTAGE).matches(view)) {
Log.i(TAG, "View is already displayed. Returning.")
return
}

val scrollView = view.findFirstParentScrollableView(view.rootView)

when (scrollView is HorizontalScrollView) {
true -> scrollView.scrollTo(view.right + scrollView.paddingEnd, 0)
false -> scrollView?.scrollTo(0, view.bottom + scrollView.paddingBottom)
}
uiController.loopMainThreadUntilIdle()
if (!ViewMatchers.isDisplayingAtLeast(FULLY_VISIBLE_PERCENTAGE).matches(view)) {

// Try scroll in the opposite direction before failing: leftwards or upwards
when (scrollView is HorizontalScrollView) {
true -> scrollView.scrollTo(view.left - scrollView.paddingStart, 0)
false -> scrollView?.scrollTo(0, view.top - scrollView.paddingTop)
}
uiController.loopMainThreadUntilIdle()

if (!ViewMatchers.isDisplayingAtLeast(FULLY_VISIBLE_PERCENTAGE).matches(view)) {
throw PerformException.Builder()
.withActionDescription(this.description)
.withViewDescription(HumanReadables.describe(view))
.withCause(
java.lang.RuntimeException(
"Fallback scrolling to view was attempted, but the view is not displayed"
)
)
.build()
}
}
}

override fun getDescription(): String {
return "fallback scroll to"
}

companion object {
private val TAG = FallbackAutoScrollToAction::class.java.simpleName
private const val FULLY_VISIBLE_PERCENTAGE = 100
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import com.kaspersky.kaspresso.interceptors.behavior.DataBehaviorInterceptor
import com.kaspersky.kaspresso.interceptors.behavior.ViewBehaviorInterceptor
import com.kaspersky.kaspresso.interceptors.behavior.WebBehaviorInterceptor
import com.kaspersky.kaspresso.interceptors.behavior.impl.autoscroll.AutoScrollViewBehaviorInterceptor
import com.kaspersky.kaspresso.interceptors.behavior.impl.autoscroll.AutoScrollViewFallbackInterceptor
import com.kaspersky.kaspresso.interceptors.behavior.impl.autoscroll.AutoScrollWebBehaviorInterceptor
import com.kaspersky.kaspresso.interceptors.behavior.impl.failure.FailureLoggingDataBehaviorInterceptor
import com.kaspersky.kaspresso.interceptors.behavior.impl.failure.FailureLoggingViewBehaviorInterceptor
Expand Down Expand Up @@ -319,6 +320,24 @@ data class Kaspresso(
}
}

/**
* In some cases, scrolling to the view works, but the action cannot be performed due to padding in
* the scrollable view, namely
* - ScrollView or NestedScrollView
* - ListView
* - HorizontalScrollView
*
* This is a very special case. If any of the screens in your tests contains one of the aforementioned
* Views and they have padding, use this in your [Kaspresso.Builder] to avoid scrolling problems
*/
fun withAutoScrollFallback(): Builder =
this.apply {
viewBehaviorInterceptors = viewBehaviorInterceptors.apply {
val autoScrollIndex = indexOfFirst { it is AutoScrollViewBehaviorInterceptor }
add(autoScrollIndex + 1, AutoScrollViewFallbackInterceptor(autoScrollParams, libLogger))
}
}

/**
* Holds an implementation of [KautomatorWaitForIdleSettings] for external developer's usage in tests.
* If it was not specified, the default implementation is used.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,28 @@ import com.kaspersky.components.kautomator.intercept.operation.UiOperationType

interface UiScrollableActions : UiBaseActions {

val uiScrollableTransformation: UiScrollable.() -> UiScrollable

fun scrollToStart() {
view.perform(UiScrollableActionType.SCROLL_TO_START) {
val scrollable = UiScrollable(UiSelector().resourceId(resourceName))
scrollable.setAsVerticalList()
scrollable.uiScrollableTransformation()
scrollable.flingToBeginning(Int.MAX_VALUE)
}
}

fun scrollToEnd() {
view.perform(UiScrollableActionType.SCROLL_TO_END) {
val scrollable = UiScrollable(UiSelector().resourceId(resourceName))
scrollable.setAsVerticalList()
scrollable.uiScrollableTransformation()
scrollable.flingToEnd(Int.MAX_VALUE)
}
}

fun <T> scrollToView(to: UiBaseView<T>) {
view.perform(UiScrollableActionType.SCROLL_TO_VIEW) {
val scrollable = UiScrollable(UiSelector().resourceId(resourceName))
scrollable.uiScrollableTransformation()
do {
if (findObject(to.view.interaction.selector.bySelector) != null)
return@perform
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@file:Suppress("unused")

package com.kaspersky.components.kautomator.component.scroll

import androidx.test.uiautomator.UiScrollable
import com.kaspersky.components.kautomator.component.common.actions.UiScrollableActions
import com.kaspersky.components.kautomator.component.common.actions.UiSwipeableActions
import com.kaspersky.components.kautomator.component.common.builders.UiViewBuilder
import com.kaspersky.components.kautomator.component.common.builders.UiViewSelector
import com.kaspersky.components.kautomator.component.common.views.UiBaseView

class UiHorizontalScrollView : UiBaseView<UiScrollView>, UiSwipeableActions, UiScrollableActions {
constructor(selector: UiViewSelector) : super(selector)
constructor(builder: UiViewBuilder.() -> Unit) : super(builder)

override val uiScrollableTransformation: UiScrollable.() -> UiScrollable
get() = { setAsHorizontalList() }
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
@file:Suppress("unused")

package com.kaspersky.components.kautomator.component.scroll

import androidx.test.uiautomator.UiScrollable
import com.kaspersky.components.kautomator.component.common.actions.UiScrollableActions
import com.kaspersky.components.kautomator.component.common.actions.UiSwipeableActions
import com.kaspersky.components.kautomator.component.common.builders.UiViewBuilder
Expand All @@ -10,4 +12,7 @@ import com.kaspersky.components.kautomator.component.common.views.UiBaseView
class UiScrollView : UiBaseView<UiScrollView>, UiSwipeableActions, UiScrollableActions {
constructor(selector: UiViewSelector) : super(selector)
constructor(builder: UiViewBuilder.() -> Unit) : super(builder)

override val uiScrollableTransformation: UiScrollable.() -> UiScrollable
get() = { setAsVerticalList() }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.kaspersky.kaspressample.screen

import com.kaspersky.kaspressample.R
import com.kaspersky.kaspressample.scrollresolver.ScrollResolverActivity
import com.kaspersky.kaspresso.screens.KScreen
import io.github.kakaocup.kakao.text.KButton

object KScrollViewWithPaddingScreen : KScreen<KScrollViewWithPaddingScreen>() {

override val layoutId: Int? = R.layout.activity_scrollview_with_padding
override val viewClass: Class<*>? = ScrollResolverActivity::class.java

val hbutton1 = KButton { withId(R.id.hvText1) }
val hbutton7 = KButton { withId(R.id.hvText7) }
val button1 = KButton { withId(R.id.tvText1) }
val button18 = KButton { withId(R.id.tvText18) }
val button20 = KButton { withId(R.id.tvText20) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ object MainScreen : KScreen<MainScreen>() {
override val layoutId: Int? = R.layout.activity_main
override val viewClass: Class<*>? = MainActivity::class.java

val scrollResolverButton = KButton { withId(R.id.activity_main_auto_scroll_fallback_button) }

val simpleButton = KButton { withId(R.id.activity_main_simple_sample_button) }

val webViewButton = KButton { withId(R.id.activity_main_webview_sample_button) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.kaspersky.kaspressample.screen

import com.kaspersky.components.kautomator.component.scroll.UiHorizontalScrollView
import com.kaspersky.components.kautomator.component.scroll.UiScrollView
import com.kaspersky.components.kautomator.component.text.UiButton
import com.kaspersky.components.kautomator.screen.UiScreen

object UiScrollViewWithPaddingScreen : UiScreen<UiScrollViewWithPaddingScreen>() {

override val packageName: String = "com.kaspersky.kaspressample"

val hScrollView = UiHorizontalScrollView { withId(this@UiScrollViewWithPaddingScreen.packageName, "hscroll_view") }

val scrollView = UiScrollView { withId(this@UiScrollViewWithPaddingScreen.packageName, "scroll_view") }

val hbutton1 = UiButton { withId(this@UiScrollViewWithPaddingScreen.packageName, "hvText1") }
val hbutton7 = UiButton { withId(this@UiScrollViewWithPaddingScreen.packageName, "hvText7") }
val button1 = UiButton { withId(this@UiScrollViewWithPaddingScreen.packageName, "tvText1") }
val button20 = UiButton { withId(this@UiScrollViewWithPaddingScreen.packageName, "tvText20") }
}
Loading