-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix dialog button click not recognized
See also android/android-test#444
- Loading branch information
Showing
2 changed files
with
73 additions
and
17 deletions.
There are no files selected for viewing
30 changes: 13 additions & 17 deletions
30
esp-android/src/main/java/de/nenick/espressomacchiato/dialog/EspAlertDialogButton.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,30 +1,26 @@ | ||
package de.nenick.espressomacchiato.dialog | ||
|
||
import android.os.Build | ||
import androidx.test.espresso.Espresso | ||
import androidx.test.espresso.matcher.RootMatchers | ||
import androidx.test.platform.app.InstrumentationRegistry | ||
import de.nenick.espressomacchiato.internals.ClickWithRetryAction | ||
import de.nenick.espressomacchiato.widget.EspButton | ||
|
||
@Suppress("UNCHECKED_CAST") | ||
class EspAlertDialogButton(id: Int, interactions: EspAlertDialogButton.() -> Unit = {}) : EspButton(id, RootMatchers.isDialog(), interactions as EspButton.() -> Unit) { | ||
class EspAlertDialogButton( | ||
id: Int, | ||
interactions: EspAlertDialogButton.() -> Unit = {} | ||
) : EspButton(id, RootMatchers.isDialog(), interactions as EspButton.() -> Unit) { | ||
|
||
override fun performClick() { | ||
fixForFastDialogTests() | ||
super.performClick() | ||
} | ||
|
||
private fun fixForFastDialogTests() { | ||
// When fast open/close multiple dialogs, the click is not always performed properly. | ||
// You see the button, but you don't see any click event happen. | ||
// https://github.com/android/android-test/issues/444 | ||
// Issue was only observed on the following android versions. | ||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { | ||
// Weird, but is enough to just give the dialog a short moment to recognize the | ||
// click properly. Perhaps the click listener isn't added yet, missing focusable? | ||
// 20ms - still flaky on fast emulator | ||
// 50ms - still flaky on slow emulator | ||
// 100ms - still flaky on slow emulator (more rarely) | ||
Thread.sleep(200) | ||
// When fast open/close multiple dialogs, the click is not always performed properly. | ||
// You see the button, but you don't see any click event happen. | ||
// https://github.com/android/android-test/issues/444 | ||
perform(ClickWithRetryAction()) | ||
Espresso.onIdle() | ||
} else { | ||
super.performClick() | ||
} | ||
} | ||
} |
60 changes: 60 additions & 0 deletions
60
esp-android/src/main/java/de/nenick/espressomacchiato/internals/ClickWithRetryAction.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package de.nenick.espressomacchiato.internals | ||
|
||
import android.view.View | ||
import androidx.test.espresso.UiController | ||
import androidx.test.espresso.ViewAction | ||
import androidx.test.espresso.matcher.ViewMatchers | ||
import org.hamcrest.Matcher | ||
|
||
class ClickWithRetryAction : ViewAction { | ||
|
||
class ClickInterceptor( | ||
private val view: View, | ||
private val originClickListener: Any, | ||
private val clickListener: () -> Unit | ||
) : View.OnClickListener { | ||
override fun onClick(v: View?) { | ||
originClickListener.javaClass.declaredMethods[0].invoke(originClickListener, view) | ||
clickListener() | ||
} | ||
} | ||
|
||
private val startTime by lazy { System.currentTimeMillis() } | ||
|
||
override fun getConstraints(): Matcher<View> = ViewMatchers.isDisplayed() | ||
|
||
override fun getDescription() = "click with retry" | ||
|
||
override fun perform(uiController: UiController, view: View) { | ||
var clickPerformed = false | ||
|
||
interceptOnClickListener(view) { | ||
clickPerformed = true | ||
} | ||
|
||
while (!clickPerformed) { | ||
if (isTimeout()) { | ||
throw InterruptedException("Click wasn't recognize.") | ||
} | ||
|
||
view.performClick() | ||
Thread.sleep(delayNextAttemptMilliseconds) | ||
} | ||
} | ||
|
||
private fun interceptOnClickListener(view: View, clickListener: () -> Unit) { | ||
val listenerInfoField = View::class.java.getDeclaredField("mListenerInfo").apply { isAccessible = true } | ||
val listenerInfo = listenerInfoField.get(view) | ||
val onClickListenerField = listenerInfo.javaClass.getDeclaredField("mOnClickListener") | ||
val originClickListener = onClickListenerField.get(listenerInfo)!! | ||
val clickInterceptor = ClickInterceptor(view, originClickListener, clickListener) | ||
onClickListenerField.set(listenerInfo, clickInterceptor) | ||
} | ||
|
||
private fun isTimeout() = System.currentTimeMillis() - startTime > timeoutMilliseconds | ||
|
||
companion object { | ||
private const val delayNextAttemptMilliseconds = 20L | ||
private const val timeoutMilliseconds = 2 * 1000L | ||
} | ||
} |