Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions detox/android/detox/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -123,31 +123,31 @@ dependencies {
// Versions are in-sync with the 'androidx-test-1.4.0' release/tag of the android-test github repo,
// used by the Detox generator. See https://github.com/android/android-test/releases/tag/androidx-test-1.4.0
// Important: Should remain so when generator tag is replaced!
api('androidx.test.espresso:espresso-core:3.6.1') {
api('androidx.test.espresso:espresso-core:3.7.0') {
because 'Needed all across Detox but also makes Espresso seamlessly provided to Detox users with hybrid apps/E2E-tests.'
}
api('androidx.test.espresso:espresso-web:3.6.1') {
api('androidx.test.espresso:espresso-web:3.7.0') {
because 'Web-View testing'
}
api('androidx.test.espresso:espresso-contrib:3.6.1') {
api('androidx.test.espresso:espresso-contrib:3.7.0') {
because 'Android datepicker support'
exclude group: "org.checkerframework", module: "checker"
}
api('org.hamcrest:hamcrest:2.2') {
because 'See https://github.com/wix/Detox/issues/3920. Need to force hamcrest 2.2 win in battle of 2.2 vs. 1.3 (specified by Espresso).'
}
api('androidx.test:rules:1.6.1') {
api('androidx.test:rules:1.7.0') {
because 'of ActivityTestRule. Needed by users *and* internally used by Detox.'
}
api('androidx.test.ext:junit:1.2.1') {
api('androidx.test.ext:junit:1.3.0') {
because 'Needed so as to seamlessly provide AndroidJUnit4 to Detox users. Depends on junit core.'
}
// Version is the latest; Cannot sync with the Github repo (e.g. android/android-test) because the androidx
// packaging version of associated classes is simply not there...
api('androidx.test.uiautomator:uiautomator:2.2.0') {
api('androidx.test.uiautomator:uiautomator:2.3.0') {
because 'Needed by Detox but also makes UIAutomator seamlessly provided to Detox users with hybrid apps/E2E-tests.'
}
api('androidx.test:core-ktx:1.6.1') {
api('androidx.test:core-ktx:1.7.0') {
because 'Needed by Detox but also makes AndroidX test core seamlessly provided to Detox users with hybrid apps/E2E-tests.'
}
implementation("org.jetbrains.kotlin:kotlin-reflect:$_kotlinVersion") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class ReactNativeIdlingResources(
fun unregisterAll() {
unregisterMQThreadsInterrogators()
unregisterIdlingResources()
syncIdlingResources()
}

fun pauseNetworkSynchronization() = pauseIdlingResource(IdlingResourcesName.Network)
Expand Down Expand Up @@ -77,8 +78,8 @@ class ReactNativeIdlingResources(
}

private fun setupMQThreadsInterrogator(looperName: LooperName) {
reactApplication.getCurrentReactContext()?.let {
val mqThreadsReflector = MQThreadsReflector(it)
reactApplication.getCurrentReactContext()?.let { reactContext ->
val mqThreadsReflector = MQThreadsReflector(reactContext)
val looper = when (looperName) {
LooperName.JS -> mqThreadsReflector.getJSMQueue()?.getLooper()
LooperName.NativeModules -> mqThreadsReflector.getNativeModulesQueue()?.getLooper()
Expand Down Expand Up @@ -108,12 +109,44 @@ class ReactNativeIdlingResources(
}

private fun unregisterMQThreadsInterrogators() {
loopers.values.forEach {
IdlingRegistry.getInstance().unregisterLooperAsIdlingResource(it)
loopers.values.forEach { looper ->
IdlingRegistry.getInstance().unregisterLooperAsIdlingResource(looper)
clearLooperHandlerCache(looper)
}
loopers.clear()
}

/**
* Workaround for Espresso 3.7.0+ where LooperIdlingResourceInterrogationHandler
* caches handlers in a static map but doesn't clear them on release().
* This causes re-registered loopers to get stale handlers with releasing=true.
*
* For older Espresso versions without this static cache, this is a no-op.
*/
private fun clearLooperHandlerCache(looper: Looper) {
try {
val handlerClass = Class.forName("androidx.test.espresso.base.LooperIdlingResourceInterrogationHandler")
val instsField = handlerClass.getDeclaredField("insts")
instsField.isAccessible = true
@Suppress("UNCHECKED_CAST")
val insts = instsField.get(null) as java.util.concurrent.ConcurrentHashMap<String, Any>

val name = String.format(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a suggestion that might help reduce the coupling to the implementation -
instead of reverse-engineering the name, iterate the map keys and look for the looper thread id

java.util.Locale.ROOT,
"LooperIdlingResource-%s-%s",
looper.thread.id,
looper.thread.name
)
insts.remove(name)
} catch (_: ClassNotFoundException) {
// Expected for older Espresso versions - silently ignore
} catch (_: NoSuchFieldException) {
// Expected for Espresso versions with different implementation - silently ignore
} catch (e: Exception) {
Log.w(LOG_TAG, "Failed to clear looper handler cache", e)
}
}

@OptIn(ExperimentalStdlibApi::class)
private fun unregisterIdlingResources() {
IdlingResourcesName.entries.forEach {
Expand Down
2 changes: 1 addition & 1 deletion detox/test/e2e/detox.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ const config = {
type: 'android.emulator',
headless: Boolean(process.env.CI),
device: {
avdName: 'Pixel_3a_API_35'
avdName: 'Pixel_3a_API_36'
},
utilBinaryPaths: ["e2e/util-binary/detoxbutler-1.1.0-aosp-release.apk"],
systemUI: {
Expand Down