diff --git a/app/src/androidTest/java/org/fnives/test/showcase/rule/ScreenshotTest.kt b/app/src/androidTest/java/org/fnives/test/showcase/rule/ScreenshotTest.kt index b124995..747b5a2 100644 --- a/app/src/androidTest/java/org/fnives/test/showcase/rule/ScreenshotTest.kt +++ b/app/src/androidTest/java/org/fnives/test/showcase/rule/ScreenshotTest.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.Lifecycle import androidx.test.core.app.ActivityScenario import androidx.test.ext.junit.runners.AndroidJUnit4 import org.fnives.test.showcase.android.testutil.activity.SafeCloseActivityRule +import org.fnives.test.showcase.android.testutil.intent.DismissSystemDialogsRule import org.fnives.test.showcase.android.testutil.screenshot.ScreenshotRule import org.fnives.test.showcase.android.testutil.synchronization.MainDispatcherTestRule import org.fnives.test.showcase.testutils.ReloadKoinModulesIfNecessaryTestRule @@ -24,7 +25,8 @@ class ScreenshotTest : KoinTest { @Rule @JvmField - val ruleOrder: RuleChain = RuleChain.outerRule(mainDispatcherTestRule) + val ruleOrder: RuleChain = RuleChain.outerRule(DismissSystemDialogsRule()) + .around(mainDispatcherTestRule) .around(ReloadKoinModulesIfNecessaryTestRule()) .around(SafeCloseActivityRule { activityScenario }) .around(ScreenshotRule(prefix = "screenshot-rule", takeOnSuccess = true)) diff --git a/app/src/androidTest/java/org/fnives/test/showcase/ui/AuthComposeInstrumentedTest.kt b/app/src/androidTest/java/org/fnives/test/showcase/ui/AuthComposeInstrumentedTest.kt index f2666d7..2711685 100644 --- a/app/src/androidTest/java/org/fnives/test/showcase/ui/AuthComposeInstrumentedTest.kt +++ b/app/src/androidTest/java/org/fnives/test/showcase/ui/AuthComposeInstrumentedTest.kt @@ -4,6 +4,7 @@ import androidx.compose.ui.test.junit4.StateRestorationTester import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import org.fnives.test.showcase.R +import org.fnives.test.showcase.android.testutil.intent.DismissSystemDialogsRule import org.fnives.test.showcase.android.testutil.screenshot.ScreenshotRule import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.anyResourceIdling import org.fnives.test.showcase.compose.screen.AppNavigation @@ -33,7 +34,8 @@ class AuthComposeInstrumentedTest : KoinTest { @Rule @JvmField - val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule) + val ruleOrder: RuleChain = RuleChain.outerRule(DismissSystemDialogsRule()) + .around(mockServerScenarioSetupTestRule) .around(dispatcherTestRule) .around(composeTestRule) .around(ScreenshotRule("test-showcase-compose")) diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityInstrumentedTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityInstrumentedTest.kt index 5e44713..7b4af06 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityInstrumentedTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityInstrumentedTest.kt @@ -5,6 +5,7 @@ import androidx.test.espresso.intent.Intents import androidx.test.ext.junit.runners.AndroidJUnit4 import org.fnives.test.showcase.android.testutil.activity.SafeCloseActivityRule import org.fnives.test.showcase.android.testutil.activity.safeClose +import org.fnives.test.showcase.android.testutil.intent.DismissSystemDialogsRule import org.fnives.test.showcase.android.testutil.screenshot.ScreenshotRule import org.fnives.test.showcase.android.testutil.synchronization.loopMainThreadFor import org.fnives.test.showcase.model.content.FavouriteContent @@ -37,7 +38,8 @@ class MainActivityInstrumentedTest : KoinTest { @Rule @JvmField - val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule) + val ruleOrder: RuleChain = RuleChain.outerRule(DismissSystemDialogsRule()) + .around(mockServerScenarioSetupTestRule) .around(mainDispatcherTestRule) .around(AsyncDiffUtilInstantTestRule()) .around(SafeCloseActivityRule { activityScenario }) diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityInstrumentedTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityInstrumentedTest.kt index c59f43d..7d7159d 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityInstrumentedTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityInstrumentedTest.kt @@ -5,6 +5,7 @@ import androidx.test.espresso.intent.Intents import androidx.test.ext.junit.runners.AndroidJUnit4 import org.fnives.test.showcase.R import org.fnives.test.showcase.android.testutil.activity.SafeCloseActivityRule +import org.fnives.test.showcase.android.testutil.intent.DismissSystemDialogsRule import org.fnives.test.showcase.android.testutil.screenshot.ScreenshotRule import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule @@ -31,7 +32,8 @@ class AuthActivityInstrumentedTest : KoinTest { @Rule @JvmField - val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule) + val ruleOrder: RuleChain = RuleChain.outerRule(DismissSystemDialogsRule()) + .around(mockServerScenarioSetupTestRule) .around(mainDispatcherTestRule) .around(SafeCloseActivityRule { activityScenario }) .around(ScreenshotRule("test-showcase")) diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityInstrumentedTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityInstrumentedTest.kt index cd2101d..9c72866 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityInstrumentedTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityInstrumentedTest.kt @@ -5,6 +5,7 @@ import androidx.test.core.app.ActivityScenario import androidx.test.espresso.intent.Intents import androidx.test.ext.junit.runners.AndroidJUnit4 import org.fnives.test.showcase.android.testutil.activity.SafeCloseActivityRule +import org.fnives.test.showcase.android.testutil.intent.DismissSystemDialogsRule import org.fnives.test.showcase.android.testutil.screenshot.ScreenshotRule import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule @@ -31,7 +32,8 @@ class SplashActivityInstrumentedTest : KoinTest { @Rule @JvmField - val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule) + val ruleOrder: RuleChain = RuleChain.outerRule(DismissSystemDialogsRule()) + .around(mockServerScenarioSetupTestRule) .around(mainDispatcherTestRule) .around(SafeCloseActivityRule { activityScenario }) .around(ScreenshotRule("test-showcase")) diff --git a/gradlescripts/versions.gradle b/gradlescripts/versions.gradle index ff127d6..6859a4e 100644 --- a/gradlescripts/versions.gradle +++ b/gradlescripts/versions.gradle @@ -33,4 +33,5 @@ project.ext { robolectric_version = "4.7" espresso_version = "3.4.0" hamcrest_version = "2.2" + ui_animator_version = "2.2.0" } \ No newline at end of file diff --git a/test-util-android/build.gradle b/test-util-android/build.gradle index c379b2b..9450a8f 100644 --- a/test-util-android/build.gradle +++ b/test-util-android/build.gradle @@ -50,6 +50,7 @@ dependencies { implementation "com.google.android.material:material:$androidx_material_version" implementation "androidx.swiperefreshlayout:swiperefreshlayout:$androidx_swiperefreshlayout_version" implementation "androidx.core:core-ktx:$androidx_core_version" + implementation "androidx.test.uiautomator:uiautomator:$ui_animator_version" } ext.artifactId = "android" diff --git a/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/intent/DismissSystemDialogsRule.kt b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/intent/DismissSystemDialogsRule.kt new file mode 100644 index 0000000..72f046b --- /dev/null +++ b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/intent/DismissSystemDialogsRule.kt @@ -0,0 +1,79 @@ +package org.fnives.test.showcase.android.testutil.intent + +import android.util.Log +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiSelector +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** + * Test Rule to workaround ANRs from other applications. + * + * ANRs on Emulators can cause or Test Activity to not receive focus and fail our Tests. + * To workaround this, this TestRule, checks the Espresso exception, if found dismisses the ANR and reruns our test. + * + * This Test Rule should be applied before every other setup, because when retrying the setup should still be clean. + */ +class DismissSystemDialogsRule( + private val anrLimit: Int = 3, + private val anrPossibleWaitMessages: Set = defaultANRPossibleWaitMessages, +) : TestRule { + + override fun apply(base: Statement, description: Description): Statement = object : Statement() { + override fun evaluate() { + var anrCount = 0 + var testFinished = false + while (!testFinished) { + try { + log("Run test method = ${description.testName}, anrCount = $anrCount") + base.evaluate() + testFinished = true + log("Test success = ${description.testName}, anrCount = $anrCount") + } catch (throwable: Throwable) { + if (throwable.isANRDialog() && anrCount < anrLimit) { + log("ANR found = ${description.testName}, anrCount = $anrCount") + anrCount++ + handleAnrDialogue() + } else { + log("Exception found = ${description.testName}, anrCount = $anrCount") + throw throwable + } + } + } + } + } + + private fun Throwable.isANRDialog() = + message?.contains(ANR_DIALOG_ESPRESSO_EXCEPTION_MESSAGE) == true + + private fun handleAnrDialogue() { + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + anrPossibleWaitMessages.first { + val waitButton = device.findObject(UiSelector().textContains(it)) + val exists = waitButton.exists() + if (exists) { + waitButton.click() + } + exists + } + } + + fun log(message: String) { + Log.d(TAG, message) + } + + companion object { + const val TAG = "DismissSysDialog" + private val Description.testName get() = "${testClass.simpleName}:$methodName" + + private val defaultANRPossibleWaitMessages = setOf( + "wait", // en + "待機", // jp + "ok" // en + ) + private const val ANR_DIALOG_ESPRESSO_EXCEPTION_MESSAGE = "Waited for the root of the view hierarchy " + + "to have window focus and not request layout for 10 seconds. If you specified a non default root matcher," + } +}