From 7d642e456a135436ae5a715d0a7d6c9092d24d66 Mon Sep 17 00:00:00 2001 From: Ray Ryan Date: Tue, 14 Mar 2023 16:35:37 -0700 Subject: [PATCH] Eliminates espresso hacks Trying to reduce Espresso flakes by being more careful and conventional about `RootMatcher` use; about targetting back button presses; and by using `recreate()` for configuration changes. --- .../sample/poetryapp/PoetryAppTest.kt | 9 +- .../squareup/sample/ravenapp/RavenAppTest.kt | 8 +- .../HelloBackButtonEspressoTest.kt | 29 +-- .../squareup/sample/dungeon/DungeonAppTest.kt | 8 +- .../HelloWorkflowFragmentAppTest.kt | 12 +- .../helloworkflow/HelloWorkflowAppTest.kt | 12 +- .../stubvisibility/StubVisibilityAppTest.kt | 16 +- .../squareup/sample/TicTacToeEspressoTest.kt | 175 +++++++++--------- .../sample/mainactivity/TodoAppTest.kt | 22 +-- .../BackStackContainerLifecycleActivity.kt | 10 +- .../api/internal-testing-android.api | 4 - .../workflow1/ui/internal/test/Espresso.kt | 56 ------ 12 files changed, 144 insertions(+), 217 deletions(-) diff --git a/samples/containers/app-poetry/src/androidTest/java/com/squareup/sample/poetryapp/PoetryAppTest.kt b/samples/containers/app-poetry/src/androidTest/java/com/squareup/sample/poetryapp/PoetryAppTest.kt index 6bc56540d7..5701642b4b 100644 --- a/samples/containers/app-poetry/src/androidTest/java/com/squareup/sample/poetryapp/PoetryAppTest.kt +++ b/samples/containers/app-poetry/src/androidTest/java/com/squareup/sample/poetryapp/PoetryAppTest.kt @@ -1,14 +1,13 @@ package com.squareup.sample.poetryapp +import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import com.squareup.sample.container.poetryapp.R -import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule -import com.squareup.workflow1.ui.internal.test.inAnyView import leakcanary.DetectLeaksAfterTestSuccess import org.junit.Rule import org.junit.Test @@ -16,17 +15,15 @@ import org.junit.rules.RuleChain import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -@OptIn(WorkflowUiExperimentalApi::class) class PoetryAppTest { private val scenarioRule = ActivityScenarioRule(PoetryActivity::class.java) - @get:Rule val rules = RuleChain.outerRule(DetectLeaksAfterTestSuccess()) + @get:Rule val rules: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess()) .around(scenarioRule) .around(IdlingDispatcherRule) @Test fun launches() { - inAnyView(withText(R.string.poems)) - .check(matches(isDisplayed())) + onView(withText(R.string.poems)).check(matches(isDisplayed())) } } diff --git a/samples/containers/app-raven/src/androidTest/java/com/squareup/sample/ravenapp/RavenAppTest.kt b/samples/containers/app-raven/src/androidTest/java/com/squareup/sample/ravenapp/RavenAppTest.kt index b1644b1e95..e7ad5d0822 100644 --- a/samples/containers/app-raven/src/androidTest/java/com/squareup/sample/ravenapp/RavenAppTest.kt +++ b/samples/containers/app-raven/src/androidTest/java/com/squareup/sample/ravenapp/RavenAppTest.kt @@ -1,13 +1,12 @@ package com.squareup.sample.ravenapp +import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule -import com.squareup.workflow1.ui.internal.test.inAnyView import leakcanary.DetectLeaksAfterTestSuccess import org.junit.Rule import org.junit.Test @@ -15,17 +14,16 @@ import org.junit.rules.RuleChain import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -@OptIn(WorkflowUiExperimentalApi::class) class RavenAppTest { private val scenarioRule = ActivityScenarioRule(RavenActivity::class.java) - @get:Rule val rules = RuleChain.outerRule(DetectLeaksAfterTestSuccess()) + @get:Rule val rules: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess()) .around(scenarioRule) .around(IdlingDispatcherRule) @Test fun launches() { - inAnyView(withText("The Raven")) + onView(withText("The Raven")) .check(matches(isDisplayed())) } } diff --git a/samples/containers/hello-back-button/src/androidTest/java/com/squareup/sample/hellobackbutton/HelloBackButtonEspressoTest.kt b/samples/containers/hello-back-button/src/androidTest/java/com/squareup/sample/hellobackbutton/HelloBackButtonEspressoTest.kt index c353358a50..9b4772a165 100644 --- a/samples/containers/hello-back-button/src/androidTest/java/com/squareup/sample/hellobackbutton/HelloBackButtonEspressoTest.kt +++ b/samples/containers/hello-back-button/src/androidTest/java/com/squareup/sample/hellobackbutton/HelloBackButtonEspressoTest.kt @@ -1,7 +1,10 @@ package com.squareup.sample.hellobackbutton +import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.pressBack import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers.isDialog import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText @@ -9,9 +12,6 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule -import com.squareup.workflow1.ui.internal.test.actuallyPressBack -import com.squareup.workflow1.ui.internal.test.inAnyView -import com.squareup.workflow1.ui.internal.test.retryBlocking import leakcanary.DetectLeaksAfterTestSuccess import org.junit.Rule import org.junit.Test @@ -24,29 +24,32 @@ class HelloBackButtonEspressoTest { private val scenarioRule = ActivityScenarioRule(HelloBackButtonActivity::class.java) - @get:Rule val rules = RuleChain.outerRule(DetectLeaksAfterTestSuccess()) + @get:Rule val rules: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess()) .around(scenarioRule) .around(IdlingDispatcherRule) - @Test fun wrappedTakesPrecedence() = retryBlocking { - inAnyView(withId(R.id.hello_message)).apply { + @Test fun wrappedTakesPrecedence() { + onView(withId(R.id.hello_message)).apply { check(matches(withText("Able"))) perform(click()) check(matches(withText("Baker"))) perform(click()) check(matches(withText("Charlie"))) - actuallyPressBack() + perform(pressBack()) check(matches(withText("Baker"))) - actuallyPressBack() + perform(pressBack()) check(matches(withText("Able"))) } } - @Test fun outerHandlerAppliesIfWrappedHandlerIsNull() = retryBlocking { - inAnyView(withId(R.id.hello_message)).apply { - actuallyPressBack() - inAnyView(withText("Are you sure you want to do this thing?")) - .check(matches(isDisplayed())) + @Test fun outerHandlerAppliesIfWrappedHandlerIsNull() { + onView(withId(R.id.hello_message)).apply { + check(matches(isDisplayed())) + perform(pressBack()) } + + onView(withText("Are you sure you want to do this thing?")) + .inRoot(isDialog()) + .check(matches(isDisplayed())) } } diff --git a/samples/dungeon/app/src/androidTest/java/com/squareup/sample/dungeon/DungeonAppTest.kt b/samples/dungeon/app/src/androidTest/java/com/squareup/sample/dungeon/DungeonAppTest.kt index c38a2e8e02..df39036a2f 100644 --- a/samples/dungeon/app/src/androidTest/java/com/squareup/sample/dungeon/DungeonAppTest.kt +++ b/samples/dungeon/app/src/androidTest/java/com/squareup/sample/dungeon/DungeonAppTest.kt @@ -1,13 +1,12 @@ package com.squareup.sample.dungeon +import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule -import com.squareup.workflow1.ui.internal.test.inAnyView import leakcanary.DetectLeaksAfterTestSuccess import org.junit.Rule import org.junit.Test @@ -15,17 +14,16 @@ import org.junit.rules.RuleChain import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -@OptIn(WorkflowUiExperimentalApi::class) class DungeonAppTest { private val scenarioRule = ActivityScenarioRule(DungeonActivity::class.java) - @get:Rule val rules = RuleChain.outerRule(DetectLeaksAfterTestSuccess()) + @get:Rule val rules: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess()) .around(scenarioRule) .around(IdlingDispatcherRule) @Test fun loadsBoardsList() { - inAnyView(withText(R.string.boards_list_label)) + onView(withText(R.string.boards_list_label)) .check(matches(isDisplayed())) } } diff --git a/samples/hello-workflow-fragment/src/androidTest/java/com/squareup/sample/helloworkflowfragment/HelloWorkflowFragmentAppTest.kt b/samples/hello-workflow-fragment/src/androidTest/java/com/squareup/sample/helloworkflowfragment/HelloWorkflowFragmentAppTest.kt index 78ad2d60ac..7ef655ea6a 100644 --- a/samples/hello-workflow-fragment/src/androidTest/java/com/squareup/sample/helloworkflowfragment/HelloWorkflowFragmentAppTest.kt +++ b/samples/hello-workflow-fragment/src/androidTest/java/com/squareup/sample/helloworkflowfragment/HelloWorkflowFragmentAppTest.kt @@ -1,14 +1,13 @@ package com.squareup.sample.helloworkflowfragment +import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule -import com.squareup.workflow1.ui.internal.test.inAnyView import leakcanary.DetectLeaksAfterTestSuccess import org.hamcrest.Matchers.containsString import org.junit.Rule @@ -17,25 +16,24 @@ import org.junit.rules.RuleChain import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -@OptIn(WorkflowUiExperimentalApi::class) class HelloWorkflowFragmentAppTest { private val scenarioRule = ActivityScenarioRule(HelloWorkflowFragmentActivity::class.java) - @get:Rule val rules = RuleChain.outerRule(DetectLeaksAfterTestSuccess()) + @get:Rule val rules: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess()) .around(scenarioRule) .around(IdlingDispatcherRule) @Test fun togglesHelloAndGoodbye() { - inAnyView(withText(containsString("Hello"))) + onView(withText(containsString("Hello"))) .check(matches(isDisplayed())) .perform(click()) - inAnyView(withText(containsString("Goodbye"))) + onView(withText(containsString("Goodbye"))) .check(matches(isDisplayed())) .perform(click()) - inAnyView(withText(containsString("Hello"))) + onView(withText(containsString("Hello"))) .check(matches(isDisplayed())) } } diff --git a/samples/hello-workflow/src/androidTest/java/com/squareup/sample/helloworkflow/HelloWorkflowAppTest.kt b/samples/hello-workflow/src/androidTest/java/com/squareup/sample/helloworkflow/HelloWorkflowAppTest.kt index 66dbdb8559..5f688422ea 100644 --- a/samples/hello-workflow/src/androidTest/java/com/squareup/sample/helloworkflow/HelloWorkflowAppTest.kt +++ b/samples/hello-workflow/src/androidTest/java/com/squareup/sample/helloworkflow/HelloWorkflowAppTest.kt @@ -1,14 +1,13 @@ package com.squareup.sample.helloworkflow +import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule -import com.squareup.workflow1.ui.internal.test.inAnyView import leakcanary.DetectLeaksAfterTestSuccess import org.junit.Rule import org.junit.Test @@ -16,25 +15,24 @@ import org.junit.rules.RuleChain import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -@OptIn(WorkflowUiExperimentalApi::class) class HelloWorkflowAppTest { private val scenarioRule = ActivityScenarioRule(HelloWorkflowActivity::class.java) - @get:Rule val rules = RuleChain.outerRule(DetectLeaksAfterTestSuccess()) + @get:Rule val rules: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess()) .around(scenarioRule) .around(IdlingDispatcherRule) @Test fun togglesHelloAndGoodbye() { - inAnyView(withText("Hello")) + onView(withText("Hello")) .check(matches(isDisplayed())) .perform(click()) - inAnyView(withText("Goodbye")) + onView(withText("Goodbye")) .check(matches(isDisplayed())) .perform(click()) - inAnyView(withText("Hello")) + onView(withText("Hello")) .check(matches(isDisplayed())) } } diff --git a/samples/stub-visibility/src/androidTest/java/com/squareup/sample/stubvisibility/StubVisibilityAppTest.kt b/samples/stub-visibility/src/androidTest/java/com/squareup/sample/stubvisibility/StubVisibilityAppTest.kt index a5f216963a..cecb773270 100644 --- a/samples/stub-visibility/src/androidTest/java/com/squareup/sample/stubvisibility/StubVisibilityAppTest.kt +++ b/samples/stub-visibility/src/androidTest/java/com/squareup/sample/stubvisibility/StubVisibilityAppTest.kt @@ -1,5 +1,6 @@ package com.squareup.sample.stubvisibility +import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed @@ -7,9 +8,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule -import com.squareup.workflow1.ui.internal.test.inAnyView import leakcanary.DetectLeaksAfterTestSuccess import org.hamcrest.CoreMatchers.not import org.junit.Rule @@ -18,29 +17,28 @@ import org.junit.rules.RuleChain import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -@OptIn(WorkflowUiExperimentalApi::class) internal class StubVisibilityAppTest { private val scenarioRule = ActivityScenarioRule(StubVisibilityActivity::class.java) - @get:Rule val rules = RuleChain.outerRule(DetectLeaksAfterTestSuccess()) + @get:Rule val rules: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess()) .around(scenarioRule) .around(IdlingDispatcherRule) @Test fun togglesFooter() { - inAnyView(withId(R.id.should_be_wrapped)) + onView(withId(R.id.should_be_wrapped)) .check(matches(not(isDisplayed()))) - inAnyView(withText("Click to show footer")) + onView(withText("Click to show footer")) .perform(click()) - inAnyView(withId(R.id.should_be_wrapped)) + onView(withId(R.id.should_be_wrapped)) .check(matches(isDisplayed())) - inAnyView(withText("Click to hide footer")) + onView(withText("Click to hide footer")) .perform(click()) - inAnyView(withId(R.id.should_be_wrapped)) + onView(withId(R.id.should_be_wrapped)) .check(matches(not(isDisplayed()))) } } diff --git a/samples/tictactoe/app/src/androidTest/java/com/squareup/sample/TicTacToeEspressoTest.kt b/samples/tictactoe/app/src/androidTest/java/com/squareup/sample/TicTacToeEspressoTest.kt index 12bd4c2f73..00337036e5 100644 --- a/samples/tictactoe/app/src/androidTest/java/com/squareup/sample/TicTacToeEspressoTest.kt +++ b/samples/tictactoe/app/src/androidTest/java/com/squareup/sample/TicTacToeEspressoTest.kt @@ -1,13 +1,14 @@ package com.squareup.sample -import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE -import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT +import androidx.test.espresso.Espresso.onView import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.ViewInteraction import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.closeSoftKeyboard +import androidx.test.espresso.action.ViewActions.pressBack import androidx.test.espresso.action.ViewActions.typeText import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers.isDialog import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withClassName import androidx.test.espresso.matcher.ViewMatchers.withId @@ -18,11 +19,7 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import com.squareup.sample.mainactivity.TicTacToeActivity import com.squareup.sample.tictactoe.R -import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule -import com.squareup.workflow1.ui.internal.test.actuallyPressBack -import com.squareup.workflow1.ui.internal.test.inAnyView -import com.squareup.workflow1.ui.internal.test.retryBlocking import leakcanary.DetectLeaksAfterTestSuccess import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.endsWith @@ -38,140 +35,144 @@ import org.junit.runner.RunWith * integration testing — especially of modals, back stacks, back button handling, * and view state management. */ -@OptIn(WorkflowUiExperimentalApi::class) @RunWith(AndroidJUnit4::class) class TicTacToeEspressoTest { - private val scenarioRule = ActivityScenarioRule(TicTacToeActivity::class.java) - @get:Rule val rules: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess()) + @get:Rule + val rules: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess()) .around(scenarioRule) .around(IdlingDispatcherRule) + private val scenario get() = scenarioRule.scenario @Before fun setUp() { scenario.onActivity { activity -> - IdlingRegistry.getInstance() - .register(activity.idlingResource) - activity.requestedOrientation = SCREEN_ORIENTATION_PORTRAIT + IdlingRegistry.getInstance().register(activity.idlingResource) } } @After fun tearDown() { scenario.onActivity { activity -> - IdlingRegistry.getInstance() - .unregister(activity.idlingResource) + IdlingRegistry.getInstance().unregister(activity.idlingResource) } } - @Test fun configChangeReflectsWorkflowState() = retryBlocking { - inAnyView(withId(R.id.login_email)).type("bad email") - inAnyView(withId(R.id.login_button)).perform(click()) + @Test fun configChangeReflectsWorkflowState() { + onView(withId(R.id.login_email)).inRoot(isDialog()).type("bad email") + onView(withId(R.id.login_button)).inRoot(isDialog()).perform(click()) - inAnyView(withId(R.id.login_error_message)).check(matches(withText("Invalid address"))) - rotate() - inAnyView(withId(R.id.login_error_message)).check(matches(withText("Invalid address"))) + onView(withId(R.id.login_error_message)).inRoot(isDialog()) + .check(matches(withText("Invalid address"))) + scenario.recreate() + onView(withId(R.id.login_error_message)).inRoot(isDialog()) + .check(matches(withText("Invalid address"))) } - @Test fun editTextSurvivesConfigChange() = retryBlocking { - inAnyView(withId(R.id.login_email)).type("foo@bar") - inAnyView(withId(R.id.login_password)).type("password") - rotate() - inAnyView(withId(R.id.login_email)).check(matches(withText("foo@bar"))) + @Test fun editTextSurvivesConfigChange() { + onView(withId(R.id.login_email)).inRoot(isDialog()).type("foo@bar") + onView(withId(R.id.login_password)).inRoot(isDialog()).type("password") + scenario.recreate() + onView(withId(R.id.login_email)).inRoot(isDialog()).check(matches(withText("foo@bar"))) // Don't save fields that shouldn't be. - inAnyView(withId(R.id.login_password)).check(matches(withText(""))) + onView(withId(R.id.login_password)).inRoot(isDialog()).check(matches(withText(""))) } - @Test fun backStackPopRestoresViewState() = retryBlocking { + @Test fun backStackPopRestoresViewState() { // The loading screen is pushed onto the back stack. - inAnyView(withId(R.id.login_email)).type("foo@bar") - inAnyView(withId(R.id.login_password)).type("bad password") - inAnyView(withId(R.id.login_button)).perform(click()) + onView(withId(R.id.login_email)).inRoot(isDialog()).type("foo@bar") + onView(withId(R.id.login_password)).inRoot(isDialog()).type("bad password") + onView(withId(R.id.login_button)).inRoot(isDialog()).perform(click()) // Loading ends with an error, and we pop back to login. The // email should have been restored from view state. - inAnyView(withId(R.id.login_email)).check(matches(withText("foo@bar"))) - inAnyView(withId(R.id.login_error_message)) + onView(withId(R.id.login_email)).inRoot(isDialog()).check(matches(withText("foo@bar"))) + onView(withId(R.id.login_error_message)).inRoot(isDialog()) .check(matches(withText("Unknown email or invalid password"))) } - @Test fun dialogSurvivesConfigChange() = retryBlocking { - inAnyView(withId(R.id.login_email)).type("foo@bar") - inAnyView(withId(R.id.login_password)).type("password") - inAnyView(withId(R.id.login_button)).perform(click()) + @Test fun dialogSurvivesConfigChange() { + onView(withId(R.id.login_email)).inRoot(isDialog()).type("foo@bar") + onView(withId(R.id.login_password)).inRoot(isDialog()).type("password") + onView(withId(R.id.login_button)).inRoot(isDialog()).perform(click()) - inAnyView(withId(R.id.player_X)).type("Mister X") - inAnyView(withId(R.id.player_O)).type("Sister O") - inAnyView(withId(R.id.start_game)).perform(click()) + onView(withId(R.id.player_X)).inRoot(isDialog()).type("Mister X") + onView(withId(R.id.player_O)).inRoot(isDialog()).type("Sister O") + onView(withId(R.id.start_game)).inRoot(isDialog()).perform(click()) - actuallyPressBack() - inAnyView(withText("Do you really want to concede the game?")) + onGameView().perform(pressBack()) + + onView(withText("Do you really want to concede the game?")).inRoot(isDialog()) .check(matches(isDisplayed())) - rotate() - inAnyView(withText("Do you really want to concede the game?")) + scenario.recreate() + onView(withText("Do you really want to concede the game?")).inRoot(isDialog()) .check(matches(isDisplayed())) } - @Test fun canGoBackFromAlert() = retryBlocking { - inAnyView(withId(R.id.login_email)).type("foo@bar") - inAnyView(withId(R.id.login_password)).type("password") - inAnyView(withId(R.id.login_button)).perform(click()) + @Test fun canGoBackFromAlert() { + onView(withId(R.id.login_email)).inRoot(isDialog()).type("foo@bar") + onView(withId(R.id.login_password)).inRoot(isDialog()).type("password") + onView(withId(R.id.login_button)).inRoot(isDialog()).perform(click()) + + onView(withId(R.id.player_X)).inRoot(isDialog()).type("Mister X") + onView(withId(R.id.player_O)).inRoot(isDialog()).type("Sister O") + onView(withId(R.id.start_game)).inRoot(isDialog()).perform(click()) - inAnyView(withId(R.id.player_X)).type("Mister X") - inAnyView(withId(R.id.player_O)).type("Sister O") - inAnyView(withId(R.id.start_game)).perform(click()) + onGameView().perform(pressBack()) - actuallyPressBack() - inAnyView(withText("Do you really want to concede the game?")) + onView(withText("Do you really want to concede the game?")).inRoot(isDialog()) .check(matches(isDisplayed())) - inAnyView(withText("I QUIT")).perform(click()) - inAnyView(withText("Really?")) + onView(withText("I QUIT")).inRoot(isDialog()).perform(click()) + onView(withText("Really?")).inRoot(isDialog()) .check(matches(isDisplayed())) + .perform(pressBack()) - actuallyPressBack() // Click a game cell to confirm the alert went away. clickCell(0) } - @Test fun canGoBackInModalViewAndSeeRestoredViewState() = retryBlocking { + @Test fun canGoBackInModalViewAndSeeRestoredViewState() { // Log in and hit the 2fa screen. - inAnyView(withId(R.id.login_email)).type("foo@2fa") - inAnyView(withId(R.id.login_password)).type("password") - inAnyView(withId(R.id.login_button)).perform(click()) - inAnyView(withId(R.id.second_factor)).check(matches(isDisplayed())) + onView(withId(R.id.login_email)).inRoot(isDialog()).type("foo@2fa") + onView(withId(R.id.login_password)).inRoot(isDialog()).type("password") + onView(withId(R.id.login_button)).inRoot(isDialog()).perform(click()) + + // See 2nd factor, then use the back button to go back and see the login screen again. + onView(withId(R.id.second_factor)).inRoot(isDialog()) + .check(matches(isDisplayed())) + .perform(pressBack()) - // Use the back button to go back and see the login screen again. - actuallyPressBack() // Make sure edit text was restored from view state cached by the back stack container. - inAnyView(withId(R.id.login_email)).check(matches(withText("foo@2fa"))) + onView(withId(R.id.login_email)).inRoot(isDialog()).check(matches(withText("foo@2fa"))) } - @Test fun canGoBackInModalViewAfterConfigChangeAndSeeRestoredViewState() = retryBlocking { + @Test fun canGoBackInModalViewAfterConfigChangeAndSeeRestoredViewState() { // Log in and hit the 2fa screen. - inAnyView(withId(R.id.login_email)).type("foo@2fa") - inAnyView(withId(R.id.login_password)).type("password") - inAnyView(withId(R.id.login_button)).perform(click()) - inAnyView(withId(R.id.second_factor)).check(matches(isDisplayed())) + onView(withId(R.id.login_email)).inRoot(isDialog()).type("foo@2fa") + onView(withId(R.id.login_password)).inRoot(isDialog()).type("password") + onView(withId(R.id.login_button)).inRoot(isDialog()).perform(click()) + onView(withId(R.id.second_factor)).inRoot(isDialog()).check(matches(isDisplayed())) // Rotate and then use the back button to go back and see the login screen again. - rotate() - actuallyPressBack() + scenario.recreate() + onView(withId(R.id.second_factor)).inRoot(isDialog()).perform(pressBack()) + // Make sure edit text was restored from view state cached by the back stack container. - inAnyView(withId(R.id.login_email)).check(matches(withText("foo@2fa"))) + onView(withId(R.id.login_email)).inRoot(isDialog()).check(matches(withText("foo@2fa"))) } /** * On tablets this revealed a problem with SavedStateRegistry. * https://github.com/square/workflow-kotlin/pull/656#issuecomment-1027274391 */ - @Test fun fullJourney() = retryBlocking { - inAnyView(withId(R.id.login_email)).type("foo@bar") - inAnyView(withId(R.id.login_password)).type("password") - inAnyView(withId(R.id.login_button)).perform(click()) + @Test fun fullJourney() { + onView(withId(R.id.login_email)).inRoot(isDialog()).type("foo@bar") + onView(withId(R.id.login_password)).inRoot(isDialog()).type("password") + onView(withId(R.id.login_button)).inRoot(isDialog()).perform(click()) - inAnyView(withId(R.id.start_game)).perform(click()) + onView(withId(R.id.start_game)).inRoot(isDialog()).perform(click()) clickCell(0) clickCell(3) @@ -179,14 +180,20 @@ class TicTacToeEspressoTest { clickCell(4) clickCell(2) - inAnyView(withText(R.string.exit)).perform(click()) - inAnyView(withId(R.id.start_game)).check(matches(isDisplayed())) - actuallyPressBack() - inAnyView(withId(R.id.login_email)).check(matches(isDisplayed())) + onView(withText(R.string.exit)).perform(click()) + onView(withId(R.id.start_game)).inRoot(isDialog()) + .check(matches(isDisplayed())) + .perform(pressBack()) + + onView(withId(R.id.login_email)).inRoot(isDialog()).check(matches(isDisplayed())) + } + + private fun onGameView(): ViewInteraction { + return onView(withClassName(endsWith("GridLayout"))) } private fun clickCell(index: Int) { - inAnyView( + onView( allOf( withParent(withClassName(endsWith("GridLayout"))), withParentIndex(index) @@ -197,10 +204,4 @@ class TicTacToeEspressoTest { private fun ViewInteraction.type(text: String) { perform(typeText(text), closeSoftKeyboard()) } - - private fun rotate() { - scenario.onActivity { - it.requestedOrientation = SCREEN_ORIENTATION_LANDSCAPE - } - } } diff --git a/samples/todo-android/app/src/androidTest/java/com/squareup/sample/mainactivity/TodoAppTest.kt b/samples/todo-android/app/src/androidTest/java/com/squareup/sample/mainactivity/TodoAppTest.kt index 4f88c66763..327d14b547 100644 --- a/samples/todo-android/app/src/androidTest/java/com/squareup/sample/mainactivity/TodoAppTest.kt +++ b/samples/todo-android/app/src/androidTest/java/com/squareup/sample/mainactivity/TodoAppTest.kt @@ -1,6 +1,8 @@ package com.squareup.sample.mainactivity +import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.pressBack import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId @@ -11,10 +13,7 @@ import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.uiautomator.UiDevice import com.squareup.sample.todo.R import com.squareup.sample.todo.ToDoActivity -import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule -import com.squareup.workflow1.ui.internal.test.actuallyPressBack -import com.squareup.workflow1.ui.internal.test.inAnyView import leakcanary.DetectLeaksAfterTestSuccess import org.hamcrest.Matchers.allOf import org.junit.After @@ -25,12 +24,11 @@ import org.junit.rules.RuleChain import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -@OptIn(WorkflowUiExperimentalApi::class) class TodoAppTest { private val scenarioRule = ActivityScenarioRule(ToDoActivity::class.java) - @get:Rule val rules = RuleChain.outerRule(DetectLeaksAfterTestSuccess()) + @get:Rule val rules: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess()) .around(scenarioRule) .around(IdlingDispatcherRule) private val uiDevice by lazy { UiDevice.getInstance(getInstrumentation()) } @@ -49,13 +47,13 @@ class TodoAppTest { val isPortrait = uiDevice.displayWidth < uiDevice.displayHeight if (!isPortrait) uiDevice.setOrientationLeft() - inAnyView(withText("Daily Chores")) + onView(withText("Daily Chores")) .check(matches(allOf(isDisplayed()))) .perform(click()) - inAnyView(withId(R.id.item_container)) + onView(withId(R.id.item_container)) .check(matches(isDisplayed())) - actuallyPressBack() - inAnyView(withId(R.id.todo_lists_container)) + .perform(pressBack()) + onView(withId(R.id.todo_lists_container)) .check(matches(isDisplayed())) } @@ -63,12 +61,12 @@ class TodoAppTest { val isPortrait = uiDevice.displayWidth < uiDevice.displayHeight if (isPortrait) uiDevice.setOrientationLeft() - inAnyView(withText("Daily Chores")) + onView(withText("Daily Chores")) .check(matches(allOf(isDisplayed()))) .perform(click()) - inAnyView(withId(R.id.item_container)) + onView(withId(R.id.item_container)) .check(matches(isDisplayed())) - inAnyView(withId(R.id.todo_lists_container)) + onView(withId(R.id.todo_lists_container)) .check(matches(isDisplayed())) } } diff --git a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/fixtures/BackStackContainerLifecycleActivity.kt b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/fixtures/BackStackContainerLifecycleActivity.kt index 65b3b1f8d7..b90ae9d4d0 100644 --- a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/fixtures/BackStackContainerLifecycleActivity.kt +++ b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/fixtures/BackStackContainerLifecycleActivity.kt @@ -5,6 +5,7 @@ import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed import androidx.test.espresso.matcher.ViewMatchers.withTagValue @@ -22,7 +23,6 @@ import com.squareup.workflow1.ui.container.fixtures.BackStackContainerLifecycleA import com.squareup.workflow1.ui.container.fixtures.BackStackContainerLifecycleActivity.TestRendering.OuterRendering import com.squareup.workflow1.ui.container.fixtures.BackStackContainerLifecycleActivity.TestRendering.RecurseRendering import com.squareup.workflow1.ui.internal.test.AbstractLifecycleTestActivity -import com.squareup.workflow1.ui.internal.test.inAnyView import org.hamcrest.Matcher import org.hamcrest.Matchers.equalTo @@ -39,9 +39,8 @@ internal class BackStackContainerLifecycleActivity : AbstractLifecycleTestActivi initialEnvironment: ViewEnvironment, context: Context, container: ViewGroup? - ): ScreenViewHolder = View(context).let { view -> - ScreenViewHolder(initialEnvironment, view) { _, _ -> /* Noop */ } - } + ): ScreenViewHolder = + ScreenViewHolder(initialEnvironment, View(context)) { _, _ -> /* Noop */ } } sealed class TestRendering : Screen { @@ -160,8 +159,7 @@ internal fun ActivityScenario.viewForScreen return view } -@OptIn(WorkflowUiExperimentalApi::class) internal fun waitForScreen(name: String) { - inAnyView(withTagValue(equalTo(name)) as Matcher) + onView(withTagValue(equalTo(name)) as Matcher) .check(matches(isCompletelyDisplayed())) } diff --git a/workflow-ui/internal-testing-android/api/internal-testing-android.api b/workflow-ui/internal-testing-android/api/internal-testing-android.api index 68a9699a02..31b13f38e9 100644 --- a/workflow-ui/internal-testing-android/api/internal-testing-android.api +++ b/workflow-ui/internal-testing-android/api/internal-testing-android.api @@ -50,12 +50,8 @@ public final class com/squareup/workflow1/ui/internal/test/AbstractLifecycleTest public final class com/squareup/workflow1/ui/internal/test/EspressoKt { public static final field DEFAULT_RETRY_TIMEOUT J public static final field RETRY_POLLING_INTERVAL J - public static final fun actuallyPressBack ()V - public static final fun inAnyView (Lorg/hamcrest/Matcher;)Landroidx/test/espresso/ViewInteraction; public static final fun retry (Ljava/lang/String;JLkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun retry$default (Ljava/lang/String;JLkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public static final fun retryBlocking (Ljava/lang/String;JLkotlin/jvm/functions/Function0;)V - public static synthetic fun retryBlocking$default (Ljava/lang/String;JLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V } public final class com/squareup/workflow1/ui/internal/test/IdleAfterTestRule : org/junit/rules/TestRule { diff --git a/workflow-ui/internal-testing-android/src/main/java/com/squareup/workflow1/ui/internal/test/Espresso.kt b/workflow-ui/internal-testing-android/src/main/java/com/squareup/workflow1/ui/internal/test/Espresso.kt index 1bfe9a006e..915eb9da1f 100644 --- a/workflow-ui/internal-testing-android/src/main/java/com/squareup/workflow1/ui/internal/test/Espresso.kt +++ b/workflow-ui/internal-testing-android/src/main/java/com/squareup/workflow1/ui/internal/test/Espresso.kt @@ -1,56 +1,10 @@ package com.squareup.workflow1.ui.internal.test -import android.view.View -import androidx.test.espresso.Espresso import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.PerformException -import androidx.test.espresso.Root -import androidx.test.espresso.ViewInteraction -import androidx.test.espresso.action.ViewActions -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.isRoot -import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import junit.framework.AssertionFailedError import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeoutOrNull -import org.hamcrest.Description -import org.hamcrest.Matcher -import org.hamcrest.Matchers.allOf -import org.hamcrest.Matchers.any -import org.hamcrest.TypeSafeMatcher - -/** - * Fork of [Espresso.onView] that looks in all [Root]s, not just the one matched by the default - * root matcher. The default root matcher will sometimes fail to find views in our dialogs. - */ -@WorkflowUiExperimentalApi -public fun inAnyView(viewMatcher: Matcher): ViewInteraction { - return Espresso.onView(viewMatcher).inRoot(any(Root::class.java)) -} - -/** - * Fork of [Espresso.pressBack] that finds the root view of the focused window only, instead of - * using the default root matcher. This is necessary because when we are showing dialogs, - * the default matcher will sometimes match the wrong window, and the back press won't do - * anything. - */ -@WorkflowUiExperimentalApi -public fun actuallyPressBack() { - val rootHasFocusMatcher = object : TypeSafeMatcher() { - override fun describeTo(description: Description) { - description.appendText("has window focus") - } - - override fun matchesSafely(item: Root): Boolean { - return item.decorView.hasWindowFocus() - } - } - - Espresso.onView(allOf(isRoot(), isDisplayed())) - .inRoot(rootHasFocusMatcher) - .perform(ViewActions.pressBack()) -} public const val DEFAULT_RETRY_TIMEOUT: Long = 100L public const val RETRY_POLLING_INTERVAL: Long = 16L @@ -92,13 +46,3 @@ public suspend inline fun retry( throw it } } - -public inline fun retryBlocking( - clue: String = "", - timeout_ms: Long = DEFAULT_RETRY_TIMEOUT, - crossinline predicate: () -> Any -) { - runBlocking { - retry(clue, timeout_ms, predicate) - } -}