From 3c6bb89bbecd8ad99daad4cd48aa02c121c980c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Sastre=20Fl=C3=B3rez?= Date: Sun, 21 Nov 2021 23:40:36 +0100 Subject: [PATCH 01/11] First working solution for AutoScrollFallback: scrolling to views inside a scrollableView with padding --- .../AutoScrollFallbackProviderImpl.kt | 129 +++++++++ .../AutoScrollViewFallbackInterceptor.kt | 24 ++ .../kaspresso/kaspresso/Kaspresso.kt | 3 + .../ScrollViewWithPaddingFailingTest.kt | 81 ++++++ .../ScrollViewWithPaddingPassingTest.kt | 71 +++++ .../kaspressample/screen/MainScreen.kt | 2 + .../screen/ScrollViewWithPaddingScreen.kt | 16 ++ .../src/main/AndroidManifest.xml | 4 + .../kaspersky/kaspressample/MainActivity.kt | 9 + .../AutoscrollFallbackActivity.kt | 14 + .../src/main/res/layout/activity_main.xml | 9 +- .../activity_scrollview_with_padding.xml | 244 ++++++++++++++++++ .../src/main/res/values-ru/strings.xml | 3 + .../src/main/res/values/strings.xml | 3 + 14 files changed, 611 insertions(+), 1 deletion(-) create mode 100644 kaspresso/src/main/kotlin/com/kaspersky/kaspresso/autoscroll/AutoScrollFallbackProviderImpl.kt create mode 100644 kaspresso/src/main/kotlin/com/kaspersky/kaspresso/interceptors/behavior/impl/autoscroll/AutoScrollViewFallbackInterceptor.kt create mode 100644 samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/autoscrollfallback_tests/ScrollViewWithPaddingFailingTest.kt create mode 100644 samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/autoscrollfallback_tests/ScrollViewWithPaddingPassingTest.kt create mode 100644 samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/screen/ScrollViewWithPaddingScreen.kt create mode 100644 samples/kaspresso-sample/src/main/kotlin/com/kaspersky/kaspressample/autoscrollfallback/AutoscrollFallbackActivity.kt create mode 100644 samples/kaspresso-sample/src/main/res/layout/activity_scrollview_with_padding.xml diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/autoscroll/AutoScrollFallbackProviderImpl.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/autoscroll/AutoScrollFallbackProviderImpl.kt new file mode 100644 index 000000000..73e0f217f --- /dev/null +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/autoscroll/AutoScrollFallbackProviderImpl.kt @@ -0,0 +1,129 @@ +package com.kaspersky.kaspresso.autoscroll + +import android.util.Log +import android.view.View +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.ViewInteraction +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.util.HumanReadables +import com.kaspersky.kaspresso.internal.extensions.other.isAllowed +import com.kaspersky.kaspresso.logger.UiTestLogger +import com.kaspersky.kaspresso.params.AutoScrollParams +import org.hamcrest.Matcher +import org.hamcrest.Matchers +import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast + + +class AutoScrollFallbackProviderImpl( + private val params: AutoScrollParams, + private val logger: UiTestLogger +) : AutoScrollProvider { + + /** + * Invokes the given [action] and calls [scroll] if it fails. Helps in cases when test fails because of the + * need to scroll to interacted view. + * + * @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 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 scroll(interaction: ViewInteraction, action: () -> T, cachedError: Throwable): T { + return try { + interaction.perform(FallbackScrollToAction()) + logger.i("View autoScroll fallback successfully performed.") + action.invoke() + } catch (error: Throwable) { + throw cachedError + } + } +} + +class FallbackScrollToAction : ViewAction { + override fun getConstraints(): Matcher { + 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 tailrec fun findFirstParentScrollView(view: View?): View? { + return if (view is ScrollView || view is NestedScrollView || view is ListView) { + view + } else if (view?.parent != null) { + findFirstParentScrollView(view.parent as View) + } else { + null + } + } + + override fun perform(uiController: UiController, view: View) { + if (isDisplayingAtLeast(100).matches(view)) { + Log.i(TAG, "View is already displayed. Returning.") + return + } + + val scrollView = findFirstParentScrollView(view) + scrollView?.scrollTo(0, view.top - scrollView.paddingTop) + + uiController.loopMainThreadUntilIdle() + if (!isDisplayingAtLeast(100).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 "scroll fallback to" + } + + companion object { + private val TAG = FallbackScrollToAction::class.java.simpleName + } +} + + diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/interceptors/behavior/impl/autoscroll/AutoScrollViewFallbackInterceptor.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/interceptors/behavior/impl/autoscroll/AutoScrollViewFallbackInterceptor.kt new file mode 100644 index 000000000..c86539938 --- /dev/null +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/interceptors/behavior/impl/autoscroll/AutoScrollViewFallbackInterceptor.kt @@ -0,0 +1,24 @@ +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 by AutoScrollFallbackProviderImpl(params, logger) { + + /** + * Wraps the given [action] invocation with the autoscrolling on failure. + * + * @param interaction the intercepted [ViewInteraction]. + * @param action the action to invoke. + */ + override fun intercept(interaction: ViewInteraction, action: () -> T): T = withAutoScroll(interaction, action) +} + diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/kaspresso/Kaspresso.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/kaspresso/Kaspresso.kt index 8e0b5be13..050842c72 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/kaspresso/Kaspresso.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/kaspresso/Kaspresso.kt @@ -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 @@ -863,6 +864,7 @@ data class Kaspresso( if (!::viewBehaviorInterceptors.isInitialized) viewBehaviorInterceptors = if (isAndroidRuntime) mutableListOf( AutoScrollViewBehaviorInterceptor(autoScrollParams, libLogger), + AutoScrollViewFallbackInterceptor(autoScrollParams, libLogger), SystemDialogSafetyViewBehaviorInterceptor( libLogger, instrumentalDependencyProviderFactory.getInterceptorProvider(instrumentation), @@ -872,6 +874,7 @@ data class Kaspresso( FailureLoggingViewBehaviorInterceptor(libLogger) ) else mutableListOf( AutoScrollViewBehaviorInterceptor(autoScrollParams, libLogger), + AutoScrollViewFallbackInterceptor(autoScrollParams, libLogger), FlakySafeViewBehaviorInterceptor(flakySafetyParams, libLogger), FailureLoggingViewBehaviorInterceptor(libLogger) ) diff --git a/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/autoscrollfallback_tests/ScrollViewWithPaddingFailingTest.kt b/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/autoscrollfallback_tests/ScrollViewWithPaddingFailingTest.kt new file mode 100644 index 000000000..73b2a46fe --- /dev/null +++ b/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/autoscrollfallback_tests/ScrollViewWithPaddingFailingTest.kt @@ -0,0 +1,81 @@ +package com.kaspersky.kaspressample.autoscrollfallback_tests + +import android.Manifest +import androidx.test.rule.ActivityTestRule +import androidx.test.rule.GrantPermissionRule +import com.kaspersky.kaspressample.MainActivity +import com.kaspersky.kaspressample.screen.MainScreen +import com.kaspersky.kaspressample.screen.ScrollViewWithPaddingScreen +import com.kaspersky.kaspresso.interceptors.behavior.impl.autoscroll.AutoScrollViewFallbackInterceptor +import com.kaspersky.kaspresso.kaspresso.Kaspresso +import com.kaspersky.kaspresso.testcases.api.testcase.TestCase +import org.junit.Rule +import org.junit.Test + +class ScrollViewWithPaddingFailingTest : TestCase( + kaspressoBuilder = Kaspresso.Builder.simple().apply { + viewBehaviorInterceptors = viewBehaviorInterceptors.apply { + val autoScrollFallback = first { it is AutoScrollViewFallbackInterceptor } + remove(autoScrollFallback) + } + } +) { + + @get:Rule + val runtimePermissionRule: GrantPermissionRule = GrantPermissionRule.grant( + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE + ) + + @get:Rule + val activityTestRule = ActivityTestRule(MainActivity::class.java, true, false) + + @Test + fun test1() = + run { + step("Open Auto Scroll Fallback Screen") { + activityTestRule.launchActivity(null) + testLogger.i("I am testLogger") + device.screenshots.take("Additional_screenshot") + MainScreen { + autoScrollFallbackButton { + isVisible() + click() + } + } + } + + step("Click button_18, middle item") { + ScrollViewWithPaddingScreen { + button18 { + click() + } + } + } + } + + @Test + fun test2() = + run { + step("Open Auto Scroll Fallback Screen") { + activityTestRule.launchActivity(null) + testLogger.i("I am testLogger") + device.screenshots.take("Additional_screenshot") + MainScreen { + autoScrollFallbackButton { + isVisible() + click() + } + } + } + + step("Click button_20, last item") { + ScrollViewWithPaddingScreen { + button20 { + click() + } + } + } + } +} + diff --git a/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/autoscrollfallback_tests/ScrollViewWithPaddingPassingTest.kt b/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/autoscrollfallback_tests/ScrollViewWithPaddingPassingTest.kt new file mode 100644 index 000000000..8a9237b5e --- /dev/null +++ b/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/autoscrollfallback_tests/ScrollViewWithPaddingPassingTest.kt @@ -0,0 +1,71 @@ +package com.kaspersky.kaspressample.autoscrollfallback_tests + +import android.Manifest +import androidx.test.rule.ActivityTestRule +import androidx.test.rule.GrantPermissionRule +import com.kaspersky.kaspressample.MainActivity +import com.kaspersky.kaspressample.screen.MainScreen +import com.kaspersky.kaspressample.screen.ScrollViewWithPaddingScreen +import com.kaspersky.kaspresso.testcases.api.testcase.TestCase +import org.junit.Rule +import org.junit.Test + +class ScrollViewWithPaddingPassingTest : TestCase() { + + @get:Rule + val runtimePermissionRule: GrantPermissionRule = GrantPermissionRule.grant( + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE + ) + + @get:Rule + val activityTestRule = ActivityTestRule(MainActivity::class.java, true, false) + + @Test + fun click_button_in_the_middle() = + run { + step("Open Auto Scroll Fallback Screen") { + activityTestRule.launchActivity(null) + testLogger.i("I am testLogger") + device.screenshots.take("Additional_screenshot") + MainScreen { + autoScrollFallbackButton { + isVisible() + click() + } + } + } + + step("Click button_18, middle item") { + ScrollViewWithPaddingScreen { + button18 { + click() + } + } + } + } + + @Test + fun click_last_button() = + run { + step("Open Auto Scroll Fallback Screen") { + activityTestRule.launchActivity(null) + testLogger.i("I am testLogger") + device.screenshots.take("Additional_screenshot") + MainScreen { + autoScrollFallbackButton { + isVisible() + click() + } + } + } + + step("Click button_20, last item") { + ScrollViewWithPaddingScreen { + button20 { + click() + } + } + } + } +} diff --git a/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/screen/MainScreen.kt b/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/screen/MainScreen.kt index 8f9eb30b0..6b40e46ec 100644 --- a/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/screen/MainScreen.kt +++ b/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/screen/MainScreen.kt @@ -11,6 +11,8 @@ object MainScreen : KScreen() { override val layoutId: Int? = R.layout.activity_main override val viewClass: Class<*>? = MainActivity::class.java + val autoScrollFallbackButton = 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) } diff --git a/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/screen/ScrollViewWithPaddingScreen.kt b/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/screen/ScrollViewWithPaddingScreen.kt new file mode 100644 index 000000000..3969e6785 --- /dev/null +++ b/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/screen/ScrollViewWithPaddingScreen.kt @@ -0,0 +1,16 @@ +package com.kaspersky.kaspressample.screen + +import com.kaspersky.kaspressample.R +import com.kaspersky.kaspressample.autoscrollfallback.AutoscrollFallbackActivity +import com.kaspersky.kaspresso.screens.KScreen +import io.github.kakaocup.kakao.text.KButton + +object ScrollViewWithPaddingScreen : KScreen() { + + override val layoutId: Int? = R.layout.activity_scrollview_with_padding + override val viewClass: Class<*>? = AutoscrollFallbackActivity::class.java + + val button18 = KButton { withId(R.id.tvText18) } + val button20 = KButton { withId(R.id.tvText20) } +} + diff --git a/samples/kaspresso-sample/src/main/AndroidManifest.xml b/samples/kaspresso-sample/src/main/AndroidManifest.xml index 6d7f90a56..3f2c89ce3 100644 --- a/samples/kaspresso-sample/src/main/AndroidManifest.xml +++ b/samples/kaspresso-sample/src/main/AndroidManifest.xml @@ -34,6 +34,10 @@ + + diff --git a/samples/kaspresso-sample/src/main/kotlin/com/kaspersky/kaspressample/MainActivity.kt b/samples/kaspresso-sample/src/main/kotlin/com/kaspersky/kaspressample/MainActivity.kt index d75df031c..07a768f78 100644 --- a/samples/kaspresso-sample/src/main/kotlin/com/kaspersky/kaspressample/MainActivity.kt +++ b/samples/kaspresso-sample/src/main/kotlin/com/kaspersky/kaspressample/MainActivity.kt @@ -3,6 +3,7 @@ package com.kaspersky.kaspressample import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import com.kaspersky.kaspressample.autoscrollfallback.AutoscrollFallbackActivity import com.kaspersky.kaspressample.compose.ComplexComposeSampleActivity import com.kaspersky.kaspressample.continuously.ContinuouslySampleActivity import com.kaspersky.kaspressample.flaky.CommonFlakyActivity @@ -20,6 +21,13 @@ class MainActivity : AppCompatActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + activity_main_auto_scroll_fallback_button.setOnClickListener { + startActivity( + Intent(this, AutoscrollFallbackActivity::class.java) + ) + } + + activity_main_simple_sample_button.setOnClickListener { startActivity( Intent(this, SimpleActivity::class.java) @@ -68,6 +76,7 @@ class MainActivity : AppCompatActivity() { ) } + activity_main_jetpack_compose_button.setOnClickListener { startActivity( Intent(this, JetpackComposeActivity::class.java) diff --git a/samples/kaspresso-sample/src/main/kotlin/com/kaspersky/kaspressample/autoscrollfallback/AutoscrollFallbackActivity.kt b/samples/kaspresso-sample/src/main/kotlin/com/kaspersky/kaspressample/autoscrollfallback/AutoscrollFallbackActivity.kt new file mode 100644 index 000000000..582924015 --- /dev/null +++ b/samples/kaspresso-sample/src/main/kotlin/com/kaspersky/kaspressample/autoscrollfallback/AutoscrollFallbackActivity.kt @@ -0,0 +1,14 @@ +package com.kaspersky.kaspressample.autoscrollfallback + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.kaspersky.kaspressample.R + +class AutoscrollFallbackActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_scrollview_with_padding) + } +} + diff --git a/samples/kaspresso-sample/src/main/res/layout/activity_main.xml b/samples/kaspresso-sample/src/main/res/layout/activity_main.xml index c167a6288..f07c58fd4 100644 --- a/samples/kaspresso-sample/src/main/res/layout/activity_main.xml +++ b/samples/kaspresso-sample/src/main/res/layout/activity_main.xml @@ -18,10 +18,17 @@ android:textSize="18pt" />