Merge pull request #110 from fknives/issue#99-system-dialog-crash-test
Issue#99 Create TestRule working around System ANRs on Emulators
This commit is contained in:
commit
d39405fda0
8 changed files with 96 additions and 5 deletions
|
|
@ -4,6 +4,7 @@ import androidx.lifecycle.Lifecycle
|
||||||
import androidx.test.core.app.ActivityScenario
|
import androidx.test.core.app.ActivityScenario
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import org.fnives.test.showcase.android.testutil.activity.SafeCloseActivityRule
|
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.screenshot.ScreenshotRule
|
||||||
import org.fnives.test.showcase.android.testutil.synchronization.MainDispatcherTestRule
|
import org.fnives.test.showcase.android.testutil.synchronization.MainDispatcherTestRule
|
||||||
import org.fnives.test.showcase.testutils.ReloadKoinModulesIfNecessaryTestRule
|
import org.fnives.test.showcase.testutils.ReloadKoinModulesIfNecessaryTestRule
|
||||||
|
|
@ -24,7 +25,8 @@ class ScreenshotTest : KoinTest {
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
@JvmField
|
@JvmField
|
||||||
val ruleOrder: RuleChain = RuleChain.outerRule(mainDispatcherTestRule)
|
val ruleOrder: RuleChain = RuleChain.outerRule(DismissSystemDialogsRule())
|
||||||
|
.around(mainDispatcherTestRule)
|
||||||
.around(ReloadKoinModulesIfNecessaryTestRule())
|
.around(ReloadKoinModulesIfNecessaryTestRule())
|
||||||
.around(SafeCloseActivityRule { activityScenario })
|
.around(SafeCloseActivityRule { activityScenario })
|
||||||
.around(ScreenshotRule(prefix = "screenshot-rule", takeOnSuccess = true))
|
.around(ScreenshotRule(prefix = "screenshot-rule", takeOnSuccess = true))
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import androidx.test.espresso.IdlingRegistry
|
||||||
import androidx.test.espresso.IdlingResource
|
import androidx.test.espresso.IdlingResource
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import org.fnives.test.showcase.R
|
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.screenshot.ScreenshotRule
|
||||||
import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.anyResourceNotIdle
|
import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.anyResourceNotIdle
|
||||||
import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.awaitUntilIdle
|
import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.awaitUntilIdle
|
||||||
|
|
@ -39,7 +40,8 @@ class AuthComposeInstrumentedTest : KoinTest {
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
@JvmField
|
@JvmField
|
||||||
val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule)
|
val ruleOrder: RuleChain = RuleChain.outerRule(DismissSystemDialogsRule())
|
||||||
|
.around(mockServerScenarioSetupTestRule)
|
||||||
.around(dispatcherTestRule)
|
.around(dispatcherTestRule)
|
||||||
.around(composeTestRule)
|
.around(composeTestRule)
|
||||||
.around(ScreenshotRule("test-showcase-compose"))
|
.around(ScreenshotRule("test-showcase-compose"))
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import androidx.test.espresso.intent.Intents
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import org.fnives.test.showcase.android.testutil.activity.SafeCloseActivityRule
|
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.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.screenshot.ScreenshotRule
|
||||||
import org.fnives.test.showcase.android.testutil.synchronization.loopMainThreadFor
|
import org.fnives.test.showcase.android.testutil.synchronization.loopMainThreadFor
|
||||||
import org.fnives.test.showcase.model.content.FavouriteContent
|
import org.fnives.test.showcase.model.content.FavouriteContent
|
||||||
|
|
@ -37,7 +38,8 @@ class MainActivityInstrumentedTest : KoinTest {
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
@JvmField
|
@JvmField
|
||||||
val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule)
|
val ruleOrder: RuleChain = RuleChain.outerRule(DismissSystemDialogsRule())
|
||||||
|
.around(mockServerScenarioSetupTestRule)
|
||||||
.around(mainDispatcherTestRule)
|
.around(mainDispatcherTestRule)
|
||||||
.around(AsyncDiffUtilInstantTestRule())
|
.around(AsyncDiffUtilInstantTestRule())
|
||||||
.around(SafeCloseActivityRule { activityScenario })
|
.around(SafeCloseActivityRule { activityScenario })
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import androidx.test.espresso.intent.Intents
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import org.fnives.test.showcase.R
|
import org.fnives.test.showcase.R
|
||||||
import org.fnives.test.showcase.android.testutil.activity.SafeCloseActivityRule
|
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.screenshot.ScreenshotRule
|
||||||
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
|
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
|
||||||
import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule
|
import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule
|
||||||
|
|
@ -31,7 +32,8 @@ class AuthActivityInstrumentedTest : KoinTest {
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
@JvmField
|
@JvmField
|
||||||
val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule)
|
val ruleOrder: RuleChain = RuleChain.outerRule(DismissSystemDialogsRule())
|
||||||
|
.around(mockServerScenarioSetupTestRule)
|
||||||
.around(mainDispatcherTestRule)
|
.around(mainDispatcherTestRule)
|
||||||
.around(SafeCloseActivityRule { activityScenario })
|
.around(SafeCloseActivityRule { activityScenario })
|
||||||
.around(ScreenshotRule("test-showcase"))
|
.around(ScreenshotRule("test-showcase"))
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import androidx.test.core.app.ActivityScenario
|
||||||
import androidx.test.espresso.intent.Intents
|
import androidx.test.espresso.intent.Intents
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import org.fnives.test.showcase.android.testutil.activity.SafeCloseActivityRule
|
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.screenshot.ScreenshotRule
|
||||||
import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule
|
import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule
|
||||||
import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule
|
import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule
|
||||||
|
|
@ -31,7 +32,8 @@ class SplashActivityInstrumentedTest : KoinTest {
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
@JvmField
|
@JvmField
|
||||||
val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule)
|
val ruleOrder: RuleChain = RuleChain.outerRule(DismissSystemDialogsRule())
|
||||||
|
.around(mockServerScenarioSetupTestRule)
|
||||||
.around(mainDispatcherTestRule)
|
.around(mainDispatcherTestRule)
|
||||||
.around(SafeCloseActivityRule { activityScenario })
|
.around(SafeCloseActivityRule { activityScenario })
|
||||||
.around(ScreenshotRule("test-showcase"))
|
.around(ScreenshotRule("test-showcase"))
|
||||||
|
|
|
||||||
|
|
@ -33,4 +33,5 @@ project.ext {
|
||||||
robolectric_version = "4.7"
|
robolectric_version = "4.7"
|
||||||
espresso_version = "3.4.0"
|
espresso_version = "3.4.0"
|
||||||
hamcrest_version = "2.2"
|
hamcrest_version = "2.2"
|
||||||
|
ui_animator_version = "2.2.0"
|
||||||
}
|
}
|
||||||
|
|
@ -50,6 +50,7 @@ dependencies {
|
||||||
implementation "com.google.android.material:material:$androidx_material_version"
|
implementation "com.google.android.material:material:$androidx_material_version"
|
||||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:$androidx_swiperefreshlayout_version"
|
implementation "androidx.swiperefreshlayout:swiperefreshlayout:$androidx_swiperefreshlayout_version"
|
||||||
implementation "androidx.core:core-ktx:$androidx_core_version"
|
implementation "androidx.core:core-ktx:$androidx_core_version"
|
||||||
|
implementation "androidx.test.uiautomator:uiautomator:$ui_animator_version"
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.artifactId = "android"
|
ext.artifactId = "android"
|
||||||
|
|
|
||||||
|
|
@ -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<String> = 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,"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue