From acc3b071f54673dede86944e1f4458f8f4e3882e Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 12 Apr 2024 11:26:37 +0100 Subject: [PATCH 1/3] Automatically scroll if click fails --- .../android/regression/FillBlankFormTest.java | 2 +- .../android/support/pages/FirstLaunchPage.kt | 27 ++++++++++++++----- .../odk/collect/android/support/pages/Page.kt | 23 +++++----------- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/regression/FillBlankFormTest.java b/collect_app/src/androidTest/java/org/odk/collect/android/regression/FillBlankFormTest.java index 903931daa21..a0a6bb76ef2 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/regression/FillBlankFormTest.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/regression/FillBlankFormTest.java @@ -575,7 +575,7 @@ public void when_scrollQuestionsList_should_questionsNotDisappear() { .copyForm("3403.xml", asList("staff_list.csv", "staff_rights.csv")) .startBlankForm("3403_ODK Version 1.23.3 Tester") .clickOnText("New Farmer Registration") - .scrollToAndClickText("Insemination") + .clickOnText("Insemination") .assertText("New Farmer Registration"); } diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FirstLaunchPage.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FirstLaunchPage.kt index cf32f73f920..dd201b85272 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FirstLaunchPage.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FirstLaunchPage.kt @@ -1,25 +1,40 @@ package org.odk.collect.android.support.pages +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.scrollTo +import androidx.test.espresso.matcher.ViewMatchers.withSubstring +import org.odk.collect.strings.R.string + class FirstLaunchPage : Page() { override fun assertOnPage(): FirstLaunchPage { - assertText(org.odk.collect.strings.R.string.configure_with_qr_code) + assertText(string.configure_with_qr_code) return this } fun clickTryCollect(): MainMenuPage { return tryAgainOnFail(MainMenuPage()) { - scrollToAndClickSubtext(org.odk.collect.strings.R.string.try_demo) + try { + onView(withSubstring(getTranslatedString(string.try_demo))).perform(click()) + } catch (e: Exception) { + onView(withSubstring(getTranslatedString(string.try_demo))) + .perform(scrollTo(), click()) + } } } fun clickManuallyEnterProjectDetails(): ManualProjectCreatorDialogPage { - scrollToAndClickText(org.odk.collect.strings.R.string.configure_manually) - return ManualProjectCreatorDialogPage().assertOnPage() + return clickOnString( + string.configure_manually, + ManualProjectCreatorDialogPage() + ) } fun clickConfigureWithQrCode(): QrCodeProjectCreatorDialogPage { - scrollToAndClickText(org.odk.collect.strings.R.string.configure_with_qr_code) - return QrCodeProjectCreatorDialogPage().assertOnPage() + return clickOnString( + string.configure_with_qr_code, + QrCodeProjectCreatorDialogPage() + ) } } diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt index 7607d6c078c..55b37d120d3 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt @@ -33,7 +33,6 @@ import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withHint import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withSubstring import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -253,7 +252,12 @@ abstract class Page> { } fun clickOnText(text: String): T { - onView(withText(text)).perform(click()) + try { + onView(withText(text)).perform(click()) + } catch (e: Exception) { + onView(withText(text)).perform(scrollTo(), click()) + } + return this as T } @@ -382,21 +386,6 @@ abstract class Page> { return this as T } - fun scrollToAndClickText(text: Int): T { - onView(withText(getTranslatedString(text))).perform(scrollTo(), click()) - return this as T - } - - fun scrollToAndClickSubtext(text: Int): T { - onView(withSubstring(getTranslatedString(text))).perform(scrollTo(), click()) - return this as T - } - - fun scrollToAndClickText(text: String?): T { - onView(withText(text)).perform(scrollTo(), click()) - return this as T - } - fun scrollToRecyclerViewItemAndClickText(text: String?): T { onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(text)), scrollTo())) onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(text)), click())) From 5c15827ae4ae1d7e17f961b68c984416e09c15cc Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 12 Apr 2024 13:25:06 +0100 Subject: [PATCH 2/3] Extract common helper for click interactions --- .../collect/android/support/Interactions.kt | 33 ++++++++++++++++++ .../android/support/pages/FirstLaunchPage.kt | 15 +++----- .../odk/collect/android/support/pages/Page.kt | 34 ++++++------------- 3 files changed, 49 insertions(+), 33 deletions(-) create mode 100644 collect_app/src/androidTest/java/org/odk/collect/android/support/Interactions.kt diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/Interactions.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/Interactions.kt new file mode 100644 index 00000000000..0baf76b544b --- /dev/null +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/Interactions.kt @@ -0,0 +1,33 @@ +package org.odk.collect.android.support + +import android.view.View +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Root +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.scrollTo +import org.hamcrest.Matcher +import org.odk.collect.android.support.WaitFor.tryAgainOnFail + +object Interactions { + + fun clickOn(view: Matcher, root: Matcher? = null) { + val onView = if (root != null) { + onView(view).inRoot(root) + } else { + onView(view) + } + + try { + onView.perform(click()) + } catch (e: Exception) { + onView.perform(scrollTo(), click()) + } + } + + fun clickOn(view: Matcher, root: Matcher? = null, assertion: () -> Unit) { + tryAgainOnFail { + clickOn(view, root) + assertion() + } + } +} diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FirstLaunchPage.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FirstLaunchPage.kt index dd201b85272..e05553a6285 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FirstLaunchPage.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FirstLaunchPage.kt @@ -1,9 +1,7 @@ package org.odk.collect.android.support.pages -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.action.ViewActions.scrollTo import androidx.test.espresso.matcher.ViewMatchers.withSubstring +import org.odk.collect.android.support.Interactions import org.odk.collect.strings.R.string class FirstLaunchPage : Page() { @@ -14,14 +12,11 @@ class FirstLaunchPage : Page() { } fun clickTryCollect(): MainMenuPage { - return tryAgainOnFail(MainMenuPage()) { - try { - onView(withSubstring(getTranslatedString(string.try_demo))).perform(click()) - } catch (e: Exception) { - onView(withSubstring(getTranslatedString(string.try_demo))) - .perform(scrollTo(), click()) - } + Interactions.clickOn(withSubstring(getTranslatedString(string.try_demo))) { + MainMenuPage().assertOnPage() } + + return MainMenuPage() } fun clickManuallyEnterProjectDetails(): ManualProjectCreatorDialogPage { diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt index 55b37d120d3..209a0d47038 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt @@ -49,6 +49,7 @@ import org.odk.collect.android.R import org.odk.collect.android.application.Collect import org.odk.collect.android.storage.StoragePathProvider import org.odk.collect.android.support.ActivityHelpers.getLaunchIntent +import org.odk.collect.android.support.Interactions import org.odk.collect.android.support.WaitFor.tryAgainOnFail import org.odk.collect.android.support.WaitFor.wait250ms import org.odk.collect.android.support.WaitFor.waitFor @@ -238,8 +239,7 @@ abstract class Page> { } fun > clickOnString(stringID: Int, destination: D): D { - tryAgainOnFail { - clickOnString(stringID) + Interactions.clickOn(withText(getTranslatedString(stringID))) { destination.assertOnPage() } @@ -252,17 +252,12 @@ abstract class Page> { } fun clickOnText(text: String): T { - try { - onView(withText(text)).perform(click()) - } catch (e: Exception) { - onView(withText(text)).perform(scrollTo(), click()) - } - + Interactions.clickOn(withText(text)) return this as T } fun clickOnId(id: Int): T { - onView(withId(id)).perform(click()) + Interactions.clickOn(withId(id)) return this as T } @@ -274,26 +269,20 @@ abstract class Page> { fun clickOKOnDialog(): T { closeSoftKeyboard() // Make sure to avoid issues with keyboard being up waitForDialogToSettle() - onView(withId(android.R.id.button1)) - .inRoot(isDialog()) - .perform(click()) + Interactions.clickOn(withId(android.R.id.button1), root = isDialog()) return this as T } fun ?> clickOKOnDialog(destination: D): D { closeSoftKeyboard() // Make sure to avoid issues with keyboard being up waitForDialogToSettle() - onView(withId(android.R.id.button1)) - .inRoot(isDialog()) - .perform(click()) + Interactions.clickOn(withId(android.R.id.button1), root = isDialog()) return destination!!.assertOnPage() } fun clickOnTextInDialog(text: String): T { waitForDialogToSettle() - onView(withText(text)) - .inRoot(isDialog()) - .perform(click()) + Interactions.clickOn(withText(text), root = isDialog()) return this as T } @@ -319,7 +308,7 @@ abstract class Page> { } fun clickOnAreaWithIndex(clazz: String?, index: Int): T { - onView(withIndex(withClassName(endsWith(clazz)), index)).perform(click()) + Interactions.clickOn(withIndex(withClassName(endsWith(clazz)), index)) return this as T } @@ -467,7 +456,7 @@ abstract class Page> { } fun closeSnackbar(): T { - onView(withContentDescription(org.odk.collect.strings.R.string.close_snackbar)).perform(click()) + Interactions.clickOn(withContentDescription(org.odk.collect.strings.R.string.close_snackbar)) return this as T } @@ -476,8 +465,7 @@ abstract class Page> { } fun clickOptionsIcon(expectedOptionString: String): T { - tryAgainOnFail { - onView(OVERFLOW_BUTTON_MATCHER).perform(click()) + Interactions.clickOn(OVERFLOW_BUTTON_MATCHER) { assertText(expectedOptionString) } @@ -521,7 +509,7 @@ abstract class Page> { } fun clickOnTextInPopup(text: Int): T { - onView(withText(text)).inRoot(isPlatformPopup()).perform(click()) + Interactions.clickOn(withText(text), root = isPlatformPopup()) return this as T } From f9e2b4722b900b21c1a6be30d0ccc1008f43af08 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 12 Apr 2024 13:39:08 +0100 Subject: [PATCH 3/3] Add docs for new methods --- .../odk/collect/android/support/Interactions.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/Interactions.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/Interactions.kt index 0baf76b544b..af7609962b9 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/Interactions.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/Interactions.kt @@ -10,6 +10,12 @@ import org.odk.collect.android.support.WaitFor.tryAgainOnFail object Interactions { + /** + * Click on the view matched by [view]. The root to use can optionally be specified with + * [root] (otherwise Espresso will use heuristics to determine the most likely root). If + * initially clicking on the view fails, this will then attempt to scroll to the view and + * retry the click. + */ fun clickOn(view: Matcher, root: Matcher? = null) { val onView = if (root != null) { onView(view).inRoot(root) @@ -24,6 +30,14 @@ object Interactions { } } + /** + * Like [clickOn], but an [assertion] can be made after the click. If this fails, the click + * action will be reattempted. + * + * This can be useful in cases where [clickOn] itself appears to succeed, but the test fails + * because the click never actually occurs (most likely due to some flakiness in + * [androidx.test.espresso.action.ViewActions.click]). + */ fun clickOn(view: Matcher, root: Matcher? = null, assertion: () -> Unit) { tryAgainOnFail { clickOn(view, root)