detox 20.43.0 → 20.44.0-smoke.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Detox-android/com/wix/detox/{20.43.0/detox-20.43.0-sources.jar → 20.44.0-smoke.1/detox-20.44.0-smoke.1-sources.jar} +0 -0
- package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1-sources.jar.md5 +1 -0
- package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1-sources.jar.sha1 +1 -0
- package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1-sources.jar.sha256 +1 -0
- package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1-sources.jar.sha512 +1 -0
- package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1.aar +0 -0
- package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1.aar.md5 +1 -0
- package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1.aar.sha1 +1 -0
- package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1.aar.sha256 +1 -0
- package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1.aar.sha512 +1 -0
- package/Detox-android/com/wix/detox/{20.43.0/detox-20.43.0.pom → 20.44.0-smoke.1/detox-20.44.0-smoke.1.pom} +1 -1
- package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1.pom.md5 +1 -0
- package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1.pom.sha1 +1 -0
- package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1.pom.sha256 +1 -0
- package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1.pom.sha512 +1 -0
- package/Detox-android/com/wix/detox/maven-metadata.xml +4 -4
- package/Detox-android/com/wix/detox/maven-metadata.xml.md5 +1 -1
- package/Detox-android/com/wix/detox/maven-metadata.xml.sha1 +1 -1
- package/Detox-android/com/wix/detox/maven-metadata.xml.sha256 +1 -1
- package/Detox-android/com/wix/detox/maven-metadata.xml.sha512 +1 -1
- package/Detox-ios-framework.tbz +0 -0
- package/Detox-ios-src.tbz +0 -0
- package/Detox-ios-xcuitest.tbz +0 -0
- package/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionHandlers.kt +7 -1
- package/android/detox/src/full/java/com/wix/detox/espresso/hierarchy/ViewHierarchyGenerator.kt +64 -0
- package/android/detox/src/full/java/com/wix/detox/inquiry/DetoxAnimationTracker.kt +70 -0
- package/android/detox/src/full/java/com/wix/detox/inquiry/DetoxFabricAnimationHook.kt +83 -0
- package/android/detox/src/full/java/com/wix/detox/inquiry/DetoxFabricIntegration.kt +99 -0
- package/android/detox/src/full/java/com/wix/detox/inquiry/DetoxFabricUIManagerWrapper.kt +37 -0
- package/android/detox/src/full/java/com/wix/detox/inquiry/ViewLifecycleRegistry.kt +233 -0
- package/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/fabric/FabricUIManagerIdlingResources.kt +170 -1
- package/package.json +2 -2
- package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0-sources.jar.md5 +0 -1
- package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0-sources.jar.sha1 +0 -1
- package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0-sources.jar.sha256 +0 -1
- package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0-sources.jar.sha512 +0 -1
- package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0.aar +0 -0
- package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0.aar.md5 +0 -1
- package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0.aar.sha1 +0 -1
- package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0.aar.sha256 +0 -1
- package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0.aar.sha512 +0 -1
- package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0.pom.md5 +0 -1
- package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0.pom.sha1 +0 -1
- package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0.pom.sha256 +0 -1
- package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0.pom.sha512 +0 -1
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bdf9863f8c4afcf4541218e9b3bed4a3
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
760a64eb6cdd8807b45b398d53de460c48df99a5
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
8e42be451e2bd115b75216749965d28317bddb694a2e880adab3abb709ba1d08
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
5cb65dc5c27e0964f4fd77c1cb638cd071b43db67100cae210c0205ea2e87b3268bfb8543c06a8baf10aa748d4eb49302db99390464b639bc4e9c8a2eeabd278
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1a39cefde2e8dde2ca54d70b0c5e5b0d
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
39e35796a4308c0a15457b1613b96b090f721432
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
76090af6a690fc00138ac3c3a307fe3cc49dfde32789bbc2d8066fa53e67805b
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
12b0c9b12325ad848ba52cdbbea6a37d837b341d5f98d6c4c34c45f357cebbfc767466df76479ebde2f1546cdc2211cfe02a3bf383371ac24b1e3de43df568b5
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<modelVersion>4.0.0</modelVersion>
|
|
4
4
|
<groupId>com.wix</groupId>
|
|
5
5
|
<artifactId>detox</artifactId>
|
|
6
|
-
<version>20.
|
|
6
|
+
<version>20.44.0-smoke.1</version>
|
|
7
7
|
<packaging>aar</packaging>
|
|
8
8
|
<name>Detox</name>
|
|
9
9
|
<description>Gray box end-to-end testing and automation library for mobile apps</description>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aae3ffc3cd9babca3385c2de23ca2be0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
b6de9c93a0a4fdc91a1e1335dc19a90d6fe592bd
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
305c59339a78c67c7fa2b6ab908c7f455d6faeb039827af94e5426238d4f464e
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3e6d2c931c52d22342ae2c2cbc0cbd4c38c74e3240a48d95cfe69a1561eb76337dbfdd4dd2753414c19f02b42e16c1fa8f2965efe9fffafce9122cd798eecf22
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
<groupId>com.wix</groupId>
|
|
4
4
|
<artifactId>detox</artifactId>
|
|
5
5
|
<versioning>
|
|
6
|
-
<latest>20.
|
|
7
|
-
<release>20.
|
|
6
|
+
<latest>20.44.0-smoke.1</latest>
|
|
7
|
+
<release>20.44.0-smoke.1</release>
|
|
8
8
|
<versions>
|
|
9
|
-
<version>20.
|
|
9
|
+
<version>20.44.0-smoke.1</version>
|
|
10
10
|
</versions>
|
|
11
|
-
<lastUpdated>
|
|
11
|
+
<lastUpdated>20251009104839</lastUpdated>
|
|
12
12
|
</versioning>
|
|
13
13
|
</metadata>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
4baad9ede6680e5d2b46f1246a48c8f9
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
b07f36a5c0ae07b6a7961b318ca15b8acc44d602
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
a95b41be51ad53267864aae6fc777db00648abe236c7bdbd46c645543e9c052f
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
03f466d7a8a4a9f3a5b82b78cc9fcfc0020dd9bb7e3c7d821096887eb241118db40ce09f2f4912a6de05bd2b4c4d75ad48133665e37f68c37bae4a3b227d08ca
|
package/Detox-ios-framework.tbz
CHANGED
|
Binary file
|
package/Detox-ios-src.tbz
CHANGED
|
Binary file
|
package/Detox-ios-xcuitest.tbz
CHANGED
|
Binary file
|
|
@@ -69,11 +69,17 @@ class InvokeActionHandler @JvmOverloads constructor(
|
|
|
69
69
|
val viewHierarchy = if (error is DetoxExceptionWithHierarchy) {
|
|
70
70
|
error.xmlHierarchy
|
|
71
71
|
} else {
|
|
72
|
-
|
|
72
|
+
generateFallbackViewHierarchy()
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
return mapOf<String, Any?>("details" to "${errorMessage}\n", "viewHierarchy" to viewHierarchy)
|
|
76
76
|
}
|
|
77
|
+
|
|
78
|
+
private fun generateFallbackViewHierarchy() = try {
|
|
79
|
+
ViewHierarchyGenerator.generateXml(shouldInjectTestIds = false)
|
|
80
|
+
} catch (e: Exception) {
|
|
81
|
+
null
|
|
82
|
+
}
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
class CleanupActionHandler(
|
package/android/detox/src/full/java/com/wix/detox/espresso/hierarchy/ViewHierarchyGenerator.kt
CHANGED
|
@@ -8,6 +8,7 @@ import android.webkit.WebView
|
|
|
8
8
|
import android.widget.TextView
|
|
9
9
|
import com.wix.detox.espresso.DeviceDisplay
|
|
10
10
|
import com.wix.detox.reactnative.ui.getAccessibilityLabel
|
|
11
|
+
import com.wix.detox.inquiry.ViewLifecycleRegistry
|
|
11
12
|
import kotlinx.coroutines.Dispatchers
|
|
12
13
|
import kotlinx.coroutines.runBlocking
|
|
13
14
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
@@ -162,6 +163,9 @@ object ViewHierarchyGenerator {
|
|
|
162
163
|
"label" to (view.getAccessibilityLabel()?.toString() ?: "")
|
|
163
164
|
)
|
|
164
165
|
|
|
166
|
+
// Add lifecycle information from ViewLifecycleRegistry
|
|
167
|
+
addLifecycleAttributes(view, attributes)
|
|
168
|
+
|
|
165
169
|
val location = IntArray(2).apply { view.getLocationInWindow(this) }
|
|
166
170
|
attributes["x"] = location[0].toString()
|
|
167
171
|
attributes["y"] = location[1].toString()
|
|
@@ -200,4 +204,64 @@ object ViewHierarchyGenerator {
|
|
|
200
204
|
View.GONE -> "gone"
|
|
201
205
|
else -> "unknown"
|
|
202
206
|
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Add lifecycle attributes to the view attributes map.
|
|
210
|
+
* This includes information about recent animations, updates, and mounts.
|
|
211
|
+
*/
|
|
212
|
+
private fun addLifecycleAttributes(view: View, attributes: MutableMap<String, String>) {
|
|
213
|
+
try {
|
|
214
|
+
// Check if view was recently animated (within last 1.5 seconds)
|
|
215
|
+
if (ViewLifecycleRegistry.wasRecentlyAnimated(view, 1500)) {
|
|
216
|
+
attributes["recentlyAnimated"] = "true"
|
|
217
|
+
|
|
218
|
+
// Add animation count if available
|
|
219
|
+
val lifecycleInfo = ViewLifecycleRegistry.getLifecycleInfo(view)
|
|
220
|
+
lifecycleInfo?.let { info ->
|
|
221
|
+
attributes["animationCount"] = info.animationCount.toString()
|
|
222
|
+
attributes["lastAnimateTime"] = info.lastAnimateTime.toString()
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Check if view was recently updated (within last 1 second)
|
|
227
|
+
if (ViewLifecycleRegistry.wasRecentlyUpdated(view, 1000)) {
|
|
228
|
+
attributes["recentlyUpdated"] = "true"
|
|
229
|
+
|
|
230
|
+
val lifecycleInfo = ViewLifecycleRegistry.getLifecycleInfo(view)
|
|
231
|
+
lifecycleInfo?.let { info ->
|
|
232
|
+
attributes["updateCount"] = info.updateCount.toString()
|
|
233
|
+
attributes["lastUpdateTime"] = info.lastUpdateTime.toString()
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Check if view was recently mounted (within last 5 seconds)
|
|
238
|
+
if (ViewLifecycleRegistry.wasRecentlyMounted(view, 5000)) {
|
|
239
|
+
attributes["recentlyMounted"] = "true"
|
|
240
|
+
|
|
241
|
+
val lifecycleInfo = ViewLifecycleRegistry.getLifecycleInfo(view)
|
|
242
|
+
lifecycleInfo?.let { info ->
|
|
243
|
+
attributes["mountTime"] = info.mountTime.toString()
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Check for problematic animations
|
|
248
|
+
if (ViewLifecycleRegistry.hasCustomEvent(view, "problematic_animation", 2000)) {
|
|
249
|
+
attributes["problematicAnimation"] = "true"
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Add any other custom events
|
|
253
|
+
val lifecycleInfo = ViewLifecycleRegistry.getLifecycleInfo(view)
|
|
254
|
+
lifecycleInfo?.let { info ->
|
|
255
|
+
if (info.customEvents.isNotEmpty()) {
|
|
256
|
+
val customEvents = info.customEvents.keys.joinToString(",")
|
|
257
|
+
attributes["customEvents"] = customEvents
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
} catch (e: Exception) {
|
|
262
|
+
// Silently ignore errors to avoid breaking the hierarchy generation
|
|
263
|
+
// Log at debug level for debugging
|
|
264
|
+
android.util.Log.d("ViewHierarchyGenerator", "Failed to add lifecycle attributes", e)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
203
267
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
package com.wix.detox.inquiry
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import com.facebook.react.bridge.ReactContext
|
|
5
|
+
import com.facebook.react.fabric.FabricUIManager
|
|
6
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
7
|
+
// import com.facebook.react.uimanager.UIManagerType
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Main entry point for Detox animation tracking in Fabric.
|
|
11
|
+
* This provides a simple API to initialize and use the animation tracking system.
|
|
12
|
+
*/
|
|
13
|
+
object DetoxAnimationTracker {
|
|
14
|
+
private const val LOG_TAG = "DetoxAnimationTracker"
|
|
15
|
+
private var isInitialized = false
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialize the animation tracking system.
|
|
19
|
+
* This should be called once when Detox starts up.
|
|
20
|
+
*/
|
|
21
|
+
fun initialize(reactContext: ReactContext) {
|
|
22
|
+
if (isInitialized) {
|
|
23
|
+
Log.d(LOG_TAG, "DetoxAnimationTracker already initialized")
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// Initialize the Fabric integration
|
|
29
|
+
DetoxFabricIntegration.initialize(reactContext)
|
|
30
|
+
isInitialized = true
|
|
31
|
+
Log.i(LOG_TAG, "DetoxAnimationTracker initialized successfully")
|
|
32
|
+
|
|
33
|
+
} catch (e: Exception) {
|
|
34
|
+
Log.e(LOG_TAG, "Failed to initialize DetoxAnimationTracker", e)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get the current animation statistics
|
|
40
|
+
*/
|
|
41
|
+
fun getAnimationStats(): Map<String, Any> {
|
|
42
|
+
return ViewLifecycleRegistry.getStats()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get all recently animated views
|
|
47
|
+
*/
|
|
48
|
+
fun getRecentlyAnimatedViews(): List<android.view.View> {
|
|
49
|
+
return ViewLifecycleRegistry.getRecentlyAnimatedViews()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if a specific view was recently animated
|
|
54
|
+
*/
|
|
55
|
+
fun wasRecentlyAnimated(view: android.view.View): Boolean {
|
|
56
|
+
return ViewLifecycleRegistry.wasRecentlyAnimated(view)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Clear all animation tracking data
|
|
61
|
+
*/
|
|
62
|
+
fun clear() {
|
|
63
|
+
ViewLifecycleRegistry.clear()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if the tracker is initialized
|
|
68
|
+
*/
|
|
69
|
+
fun isInitialized(): Boolean = isInitialized
|
|
70
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
package com.wix.detox.inquiry
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import android.view.View
|
|
5
|
+
import com.facebook.react.bridge.ReadableMap
|
|
6
|
+
import com.facebook.react.fabric.FabricUIManager
|
|
7
|
+
import com.wix.detox.inquiry.ViewLifecycleRegistry.markAnimated
|
|
8
|
+
import com.wix.detox.inquiry.ViewLifecycleRegistry.markMounted
|
|
9
|
+
import com.wix.detox.inquiry.ViewLifecycleRegistry.markUpdated
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Hook into React Native's Fabric new architecture to track animated views.
|
|
13
|
+
* This provides precise tracking by intercepting the exact points where animated
|
|
14
|
+
* properties are applied to views in Fabric.
|
|
15
|
+
*/
|
|
16
|
+
object DetoxFabricAnimationHook {
|
|
17
|
+
private const val LOG_TAG = "DetoxFabricHook"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Hook into FabricUIManager.synchronouslyUpdateViewOnUIThread to track animated updates.
|
|
21
|
+
* This marks views as animated whenever there's any animation activity, giving lots of false positives.
|
|
22
|
+
*/
|
|
23
|
+
fun hookSynchronouslyUpdateViewOnUIThread(
|
|
24
|
+
reactTag: Int,
|
|
25
|
+
props: ReadableMap?,
|
|
26
|
+
fabricUIManager: FabricUIManager
|
|
27
|
+
) {
|
|
28
|
+
try {
|
|
29
|
+
// Get the actual Android View
|
|
30
|
+
val androidView = fabricUIManager.resolveView(reactTag)
|
|
31
|
+
if (androidView == null) {
|
|
32
|
+
Log.d(LOG_TAG, "View not found for tag: $reactTag")
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
markAnimated(androidView)
|
|
37
|
+
} catch (e: Exception) {
|
|
38
|
+
Log.w(LOG_TAG, "Failed to hook animated view update", e)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Hook into view mount operations to track when views are created.
|
|
44
|
+
*/
|
|
45
|
+
fun hookViewMount(
|
|
46
|
+
reactTag: Int,
|
|
47
|
+
fabricUIManager: FabricUIManager
|
|
48
|
+
) {
|
|
49
|
+
try {
|
|
50
|
+
val androidView = fabricUIManager.resolveView(reactTag)
|
|
51
|
+
if (androidView != null) {
|
|
52
|
+
Log.d(LOG_TAG, "View mounted with tag: $reactTag")
|
|
53
|
+
markMounted(androidView)
|
|
54
|
+
}
|
|
55
|
+
} catch (e: Exception) {
|
|
56
|
+
Log.w(LOG_TAG, "Failed to hook view mount", e)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get view coordinates for highlighting
|
|
63
|
+
*/
|
|
64
|
+
fun getViewCoordinates(view: View): IntArray {
|
|
65
|
+
val coords = intArrayOf(0, 0, 0, 0)
|
|
66
|
+
try {
|
|
67
|
+
view.getLocationOnScreen(coords)
|
|
68
|
+
coords[2] = view.width
|
|
69
|
+
coords[3] = view.height
|
|
70
|
+
} catch (e: Exception) {
|
|
71
|
+
Log.w(LOG_TAG, "Failed to get view coordinates", e)
|
|
72
|
+
}
|
|
73
|
+
return coords
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Log current registry statistics
|
|
78
|
+
*/
|
|
79
|
+
fun logRegistryStats() {
|
|
80
|
+
val stats = ViewLifecycleRegistry.getStats()
|
|
81
|
+
Log.i(LOG_TAG, "ViewLifecycleRegistry stats: $stats")
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
package com.wix.detox.inquiry
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import com.facebook.react.bridge.ReactContext
|
|
5
|
+
import com.facebook.react.fabric.FabricUIManager
|
|
6
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
7
|
+
// import com.facebook.react.uimanager.UIManagerType
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Integration point for Detox with React Native's Fabric architecture.
|
|
11
|
+
* This provides hooks into Fabric's animation system to track animated views.
|
|
12
|
+
*/
|
|
13
|
+
object DetoxFabricIntegration {
|
|
14
|
+
private const val LOG_TAG = "DetoxFabricIntegration"
|
|
15
|
+
private var isInitialized = false
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialize the Fabric animation hooks.
|
|
19
|
+
* This should be called once when Detox starts up.
|
|
20
|
+
*/
|
|
21
|
+
fun initialize(reactContext: ReactContext) {
|
|
22
|
+
if (isInitialized) {
|
|
23
|
+
Log.d(LOG_TAG, "DetoxFabricIntegration already initialized")
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// Get the FabricUIManager
|
|
29
|
+
val fabricUIManager = UIManagerHelper.getUIManager(reactContext, 1) as? FabricUIManager
|
|
30
|
+
if (fabricUIManager == null) {
|
|
31
|
+
Log.w(LOG_TAG, "FabricUIManager not available - Fabric animation tracking disabled")
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Hook into the FabricUIManager
|
|
36
|
+
hookFabricUIManager(fabricUIManager)
|
|
37
|
+
isInitialized = true
|
|
38
|
+
Log.i(LOG_TAG, "DetoxFabricIntegration initialized successfully")
|
|
39
|
+
|
|
40
|
+
} catch (e: Exception) {
|
|
41
|
+
Log.e(LOG_TAG, "Failed to initialize DetoxFabricIntegration", e)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Hook into FabricUIManager to track animated updates.
|
|
47
|
+
* This uses reflection to intercept synchronouslyUpdateViewOnUIThread calls.
|
|
48
|
+
*/
|
|
49
|
+
private fun hookFabricUIManager(fabricUIManager: FabricUIManager) {
|
|
50
|
+
try {
|
|
51
|
+
// Create a wrapper that intercepts calls to synchronouslyUpdateViewOnUIThread
|
|
52
|
+
val originalMethod = FabricUIManager::class.java.getDeclaredMethod(
|
|
53
|
+
"synchronouslyUpdateViewOnUIThread",
|
|
54
|
+
Int::class.java,
|
|
55
|
+
com.facebook.react.bridge.ReadableMap::class.java
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
// Note: In a real implementation, you would use bytecode manipulation
|
|
59
|
+
// or AOP to intercept this method. For now, we'll provide a manual hook
|
|
60
|
+
// that can be called from the application code.
|
|
61
|
+
|
|
62
|
+
Log.d(LOG_TAG, "FabricUIManager hook prepared (manual integration required)")
|
|
63
|
+
|
|
64
|
+
} catch (e: Exception) {
|
|
65
|
+
Log.w(LOG_TAG, "Failed to hook FabricUIManager", e)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Manual hook for synchronouslyUpdateViewOnUIThread.
|
|
71
|
+
* This should be called from the application's FabricUIManager wrapper.
|
|
72
|
+
*/
|
|
73
|
+
fun onSynchronouslyUpdateViewOnUIThread(
|
|
74
|
+
reactTag: Int,
|
|
75
|
+
props: com.facebook.react.bridge.ReadableMap?,
|
|
76
|
+
fabricUIManager: FabricUIManager
|
|
77
|
+
) {
|
|
78
|
+
DetoxFabricAnimationHook.hookSynchronouslyUpdateViewOnUIThread(reactTag, props, fabricUIManager)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Manual hook for view mount operations.
|
|
83
|
+
*/
|
|
84
|
+
fun onViewMount(reactTag: Int, fabricUIManager: FabricUIManager) {
|
|
85
|
+
DetoxFabricAnimationHook.hookViewMount(reactTag, fabricUIManager)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if the integration is initialized
|
|
90
|
+
*/
|
|
91
|
+
fun isInitialized(): Boolean = isInitialized
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get current animation statistics
|
|
95
|
+
*/
|
|
96
|
+
fun getAnimationStats(): Map<String, Any> {
|
|
97
|
+
return ViewLifecycleRegistry.getStats()
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
package com.wix.detox.inquiry
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import com.facebook.react.bridge.ReadableMap
|
|
5
|
+
import com.facebook.react.fabric.FabricUIManager
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Wrapper for FabricUIManager that intercepts animation-related calls.
|
|
9
|
+
* This provides a clean way to hook into Fabric's animation system.
|
|
10
|
+
*/
|
|
11
|
+
class DetoxFabricUIManagerWrapper(
|
|
12
|
+
private val originalUIManager: FabricUIManager
|
|
13
|
+
) {
|
|
14
|
+
private val LOG_TAG = "DetoxFabricUIManagerWrapper"
|
|
15
|
+
|
|
16
|
+
fun synchronouslyUpdateViewOnUIThread(reactTag: Int, props: ReadableMap?) {
|
|
17
|
+
try {
|
|
18
|
+
// Call the original method first (only if props is not null)
|
|
19
|
+
if (props != null) {
|
|
20
|
+
originalUIManager.synchronouslyUpdateViewOnUIThread(reactTag, props)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Then hook our animation tracking
|
|
24
|
+
DetoxFabricAnimationHook.hookSynchronouslyUpdateViewOnUIThread(reactTag, props, originalUIManager)
|
|
25
|
+
|
|
26
|
+
} catch (e: Exception) {
|
|
27
|
+
Log.w(LOG_TAG, "Failed to process animated view update", e)
|
|
28
|
+
// Still call the original method to avoid breaking the app (only if props is not null)
|
|
29
|
+
if (props != null) {
|
|
30
|
+
originalUIManager.synchronouslyUpdateViewOnUIThread(reactTag, props)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Delegate all other methods to the original UIManager
|
|
36
|
+
fun resolveView(reactTag: Int) = originalUIManager.resolveView(reactTag)
|
|
37
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
package com.wix.detox.inquiry
|
|
2
|
+
|
|
3
|
+
import android.view.View
|
|
4
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
5
|
+
import java.util.concurrent.atomic.AtomicLong
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Registry to track various lifecycle events of Android Views.
|
|
9
|
+
* Uses WeakHashMap to prevent memory leaks and allows tracking of:
|
|
10
|
+
* - Mount events (when views are created/mounted)
|
|
11
|
+
* - Animation events (when views are animated)
|
|
12
|
+
* - Update events (when views are updated)
|
|
13
|
+
* - Custom events (extensible for future needs)
|
|
14
|
+
*/
|
|
15
|
+
object ViewLifecycleRegistry {
|
|
16
|
+
private val viewLifecycleInfo = ConcurrentHashMap<View, ViewLifecycleInfo>()
|
|
17
|
+
private val lastCleanupTime = AtomicLong(0)
|
|
18
|
+
private val cleanupIntervalMs = 30_000L // Clean up every 30 seconds
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Data class to hold lifecycle information for a view
|
|
22
|
+
*/
|
|
23
|
+
data class ViewLifecycleInfo(
|
|
24
|
+
val view: View,
|
|
25
|
+
val mountTime: Long = 0,
|
|
26
|
+
val lastAnimateTime: Long = 0,
|
|
27
|
+
val lastUpdateTime: Long = 0,
|
|
28
|
+
val animationCount: Int = 0,
|
|
29
|
+
val updateCount: Int = 0,
|
|
30
|
+
val customEvents: MutableMap<String, Long> = mutableMapOf()
|
|
31
|
+
) {
|
|
32
|
+
fun wasRecentlyAnimated(windowMs: Long = 1500): Boolean {
|
|
33
|
+
if (lastAnimateTime == 0L) return false
|
|
34
|
+
return System.currentTimeMillis() - lastAnimateTime <= windowMs
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fun wasRecentlyUpdated(windowMs: Long = 1000): Boolean {
|
|
38
|
+
if (lastUpdateTime == 0L) return false
|
|
39
|
+
return System.currentTimeMillis() - lastUpdateTime <= windowMs
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fun wasRecentlyMounted(windowMs: Long = 5000): Boolean {
|
|
43
|
+
if (mountTime == 0L) return false
|
|
44
|
+
return System.currentTimeMillis() - mountTime <= windowMs
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fun hasCustomEvent(eventType: String, windowMs: Long = 2000): Boolean {
|
|
48
|
+
val eventTime = customEvents[eventType] ?: return false
|
|
49
|
+
return System.currentTimeMillis() - eventTime <= windowMs
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Mark a view as mounted
|
|
55
|
+
*/
|
|
56
|
+
fun markMounted(view: View) {
|
|
57
|
+
val now = System.currentTimeMillis()
|
|
58
|
+
viewLifecycleInfo.compute(view) { _, existing ->
|
|
59
|
+
existing?.copy(mountTime = now) ?: ViewLifecycleInfo(view, mountTime = now)
|
|
60
|
+
}
|
|
61
|
+
performPeriodicCleanup()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Mark a view as animated
|
|
66
|
+
*/
|
|
67
|
+
fun markAnimated(view: View) {
|
|
68
|
+
val now = System.currentTimeMillis()
|
|
69
|
+
|
|
70
|
+
viewLifecycleInfo.compute(view) { _, existing ->
|
|
71
|
+
val info = existing ?: ViewLifecycleInfo(view)
|
|
72
|
+
info.copy(
|
|
73
|
+
lastAnimateTime = now,
|
|
74
|
+
animationCount = info.animationCount + 1
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
performPeriodicCleanup()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Mark a view as updated
|
|
82
|
+
*/
|
|
83
|
+
fun markUpdated(view: View) {
|
|
84
|
+
val now = System.currentTimeMillis()
|
|
85
|
+
viewLifecycleInfo.compute(view) { _, existing ->
|
|
86
|
+
val info = existing ?: ViewLifecycleInfo(view)
|
|
87
|
+
info.copy(
|
|
88
|
+
lastUpdateTime = now,
|
|
89
|
+
updateCount = info.updateCount + 1
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
performPeriodicCleanup()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Mark a custom event for a view
|
|
97
|
+
*/
|
|
98
|
+
fun markCustomEvent(view: View, eventType: String) {
|
|
99
|
+
val now = System.currentTimeMillis()
|
|
100
|
+
viewLifecycleInfo.compute(view) { _, existing ->
|
|
101
|
+
val info = existing ?: ViewLifecycleInfo(view)
|
|
102
|
+
info.customEvents[eventType] = now
|
|
103
|
+
info
|
|
104
|
+
}
|
|
105
|
+
performPeriodicCleanup()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if a view was recently animated
|
|
110
|
+
*/
|
|
111
|
+
fun wasRecentlyAnimated(view: View, windowMs: Long = 1500): Boolean {
|
|
112
|
+
return viewLifecycleInfo[view]?.wasRecentlyAnimated(windowMs) ?: false
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Check if a view was recently updated
|
|
117
|
+
*/
|
|
118
|
+
fun wasRecentlyUpdated(view: View, windowMs: Long = 1000): Boolean {
|
|
119
|
+
return viewLifecycleInfo[view]?.wasRecentlyUpdated(windowMs) ?: false
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check if a view was recently mounted
|
|
124
|
+
*/
|
|
125
|
+
fun wasRecentlyMounted(view: View, windowMs: Long = 5000): Boolean {
|
|
126
|
+
return viewLifecycleInfo[view]?.wasRecentlyMounted(windowMs) ?: false
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check if a view has a custom event within the time window
|
|
131
|
+
*/
|
|
132
|
+
fun hasCustomEvent(view: View, eventType: String, windowMs: Long = 2000): Boolean {
|
|
133
|
+
return viewLifecycleInfo[view]?.hasCustomEvent(eventType, windowMs) ?: false
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get lifecycle info for a view
|
|
138
|
+
*/
|
|
139
|
+
fun getLifecycleInfo(view: View): ViewLifecycleInfo? {
|
|
140
|
+
return viewLifecycleInfo[view]
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get all views that were recently animated
|
|
145
|
+
*/
|
|
146
|
+
fun getRecentlyAnimatedViews(windowMs: Long = 1500): List<View> {
|
|
147
|
+
return viewLifecycleInfo.keys.filter { wasRecentlyAnimated(it, windowMs) }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get all views that were recently updated
|
|
152
|
+
*/
|
|
153
|
+
fun getRecentlyUpdatedViews(windowMs: Long = 1000): List<View> {
|
|
154
|
+
return viewLifecycleInfo.keys.filter { wasRecentlyUpdated(it, windowMs) }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get all views that were recently mounted
|
|
159
|
+
*/
|
|
160
|
+
fun getRecentlyMountedViews(windowMs: Long = 5000): List<View> {
|
|
161
|
+
return viewLifecycleInfo.keys.filter { wasRecentlyMounted(it, windowMs) }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get all views with a specific custom event
|
|
166
|
+
*/
|
|
167
|
+
fun getViewsWithCustomEvent(eventType: String, windowMs: Long = 2000): List<View> {
|
|
168
|
+
return viewLifecycleInfo.keys.filter { hasCustomEvent(it, eventType, windowMs) }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get statistics about the registry
|
|
173
|
+
*/
|
|
174
|
+
fun getStats(): Map<String, Any> {
|
|
175
|
+
val totalViews = viewLifecycleInfo.size
|
|
176
|
+
val recentlyAnimated = getRecentlyAnimatedViews().size
|
|
177
|
+
val recentlyUpdated = getRecentlyUpdatedViews().size
|
|
178
|
+
val recentlyMounted = getRecentlyMountedViews().size
|
|
179
|
+
|
|
180
|
+
return mapOf(
|
|
181
|
+
"totalViews" to totalViews,
|
|
182
|
+
"recentlyAnimated" to recentlyAnimated,
|
|
183
|
+
"recentlyUpdated" to recentlyUpdated,
|
|
184
|
+
"recentlyMounted" to recentlyMounted
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Clear all data (useful for testing)
|
|
190
|
+
*/
|
|
191
|
+
fun clear() {
|
|
192
|
+
viewLifecycleInfo.clear()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Perform periodic cleanup to remove stale entries
|
|
197
|
+
*/
|
|
198
|
+
private fun performPeriodicCleanup() {
|
|
199
|
+
val now = System.currentTimeMillis()
|
|
200
|
+
val lastCleanup = lastCleanupTime.get()
|
|
201
|
+
|
|
202
|
+
if (now - lastCleanup > cleanupIntervalMs &&
|
|
203
|
+
lastCleanupTime.compareAndSet(lastCleanup, now)) {
|
|
204
|
+
|
|
205
|
+
// Remove views that are no longer valid or haven't been accessed recently
|
|
206
|
+
val iterator = viewLifecycleInfo.iterator()
|
|
207
|
+
while (iterator.hasNext()) {
|
|
208
|
+
val entry = iterator.next()
|
|
209
|
+
val view = entry.key
|
|
210
|
+
val info = entry.value
|
|
211
|
+
|
|
212
|
+
// Remove if view is no longer valid or hasn't been accessed in 5 minutes
|
|
213
|
+
if (!isViewValid(view) || (now - info.lastAnimateTime > 300_000 &&
|
|
214
|
+
now - info.lastUpdateTime > 300_000 &&
|
|
215
|
+
now - info.mountTime > 300_000)) {
|
|
216
|
+
iterator.remove()
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Check if a view is still valid (not garbage collected)
|
|
224
|
+
*/
|
|
225
|
+
private fun isViewValid(view: View): Boolean {
|
|
226
|
+
return try {
|
|
227
|
+
view.javaClass // Try to access the view
|
|
228
|
+
true
|
|
229
|
+
} catch (e: Exception) {
|
|
230
|
+
false
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
package com.wix.detox.reactnative.idlingresources.uimodule.fabric
|
|
2
2
|
|
|
3
|
+
import android.util.Log
|
|
3
4
|
import android.view.Choreographer
|
|
4
5
|
import androidx.test.espresso.IdlingResource
|
|
5
6
|
import com.facebook.react.bridge.ReactContext
|
|
6
7
|
import com.facebook.react.uimanager.UIManagerHelper
|
|
7
8
|
import com.facebook.react.uimanager.common.UIManagerType
|
|
8
9
|
import com.wix.detox.reactnative.idlingresources.DetoxIdlingResource
|
|
10
|
+
import com.wix.detox.inquiry.ViewLifecycleRegistry
|
|
9
11
|
import org.joor.Reflect
|
|
10
12
|
import java.util.concurrent.ConcurrentLinkedQueue
|
|
11
13
|
|
|
@@ -15,10 +17,15 @@ class FabricUIManagerIdlingResources(
|
|
|
15
17
|
) : DetoxIdlingResource(), Choreographer.FrameCallback {
|
|
16
18
|
|
|
17
19
|
override fun checkIdle(): Boolean {
|
|
18
|
-
|
|
20
|
+
val mountItemsSize = getMountItemsSize()
|
|
21
|
+
val viewCommandMountItemsSize = getViewCommandMountItemsSize()
|
|
22
|
+
|
|
23
|
+
return if (mountItemsSize == 0 && viewCommandMountItemsSize == 0) {
|
|
19
24
|
notifyIdle()
|
|
20
25
|
true
|
|
21
26
|
} else {
|
|
27
|
+
// Track mount items to identify animated views
|
|
28
|
+
trackMountItems()
|
|
22
29
|
Choreographer.getInstance().postFrameCallback(this)
|
|
23
30
|
false
|
|
24
31
|
}
|
|
@@ -67,4 +74,166 @@ class FabricUIManagerIdlingResources(
|
|
|
67
74
|
return viewCommandMountItems.size
|
|
68
75
|
}
|
|
69
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Track mount items to identify animated views.
|
|
79
|
+
* This is called when the UI manager is busy processing mount items.
|
|
80
|
+
*/
|
|
81
|
+
private fun trackMountItems() {
|
|
82
|
+
try {
|
|
83
|
+
val mountItemDispatcher = getMountItemDispatcher()
|
|
84
|
+
val mountItems = Reflect.on(mountItemDispatcher).field("mMountItems").get<ConcurrentLinkedQueue<*>>()
|
|
85
|
+
|
|
86
|
+
// Track each mount item to identify animated views
|
|
87
|
+
mountItems.forEach { mountItem ->
|
|
88
|
+
trackMountItem(mountItem)
|
|
89
|
+
}
|
|
90
|
+
} catch (e: Exception) {
|
|
91
|
+
// Silently ignore errors to avoid breaking the idling resource
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Track individual mount item to identify animated views.
|
|
97
|
+
*/
|
|
98
|
+
private fun trackMountItem(mountItem: Any) {
|
|
99
|
+
try {
|
|
100
|
+
val mountItemClass = mountItem.javaClass.simpleName
|
|
101
|
+
Log.i("DetoxFabricDebug", "Processing mount item: $mountItemClass")
|
|
102
|
+
|
|
103
|
+
when (mountItemClass) {
|
|
104
|
+
"IntBufferBatchMountItem" -> {
|
|
105
|
+
Log.i("DetoxFabricDebug", "Found IntBufferBatchMountItem - processing animated props")
|
|
106
|
+
// This is where animated props get applied
|
|
107
|
+
trackIntBufferBatchMountItem(mountItem)
|
|
108
|
+
}
|
|
109
|
+
"CreateMountItem" -> {
|
|
110
|
+
Log.i("DetoxFabricDebug", "Found CreateMountItem - processing view creation")
|
|
111
|
+
// Track view creation
|
|
112
|
+
trackCreateMountItem(mountItem)
|
|
113
|
+
}
|
|
114
|
+
else -> {
|
|
115
|
+
Log.i("DetoxFabricDebug", "Unknown mount item type: $mountItemClass")
|
|
116
|
+
}
|
|
117
|
+
// Add other mount item types as needed
|
|
118
|
+
}
|
|
119
|
+
} catch (e: Exception) {
|
|
120
|
+
Log.e("DetoxFabricDebug", "Error processing mount item", e)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Track IntBufferBatchMountItem which contains animated prop updates.
|
|
126
|
+
*/
|
|
127
|
+
private fun trackIntBufferBatchMountItem(mountItem: Any) {
|
|
128
|
+
try {
|
|
129
|
+
// Use reflection to access the mount item's data
|
|
130
|
+
val intBuffer = Reflect.on(mountItem).field("mIntBuffer").get<IntArray>()
|
|
131
|
+
val objBuffer = Reflect.on(mountItem).field("mObjBuffer").get<Array<Any>>()
|
|
132
|
+
|
|
133
|
+
var i = 0
|
|
134
|
+
var j = 0
|
|
135
|
+
|
|
136
|
+
while (i < intBuffer.size) {
|
|
137
|
+
val instruction = intBuffer[i++]
|
|
138
|
+
|
|
139
|
+
when (instruction) {
|
|
140
|
+
32 -> { // INSTRUCTION_UPDATE_PROPS
|
|
141
|
+
val viewTag = intBuffer[i++]
|
|
142
|
+
val props = objBuffer[j++] as? com.facebook.react.bridge.ReadableMap
|
|
143
|
+
|
|
144
|
+
// Track animated view update
|
|
145
|
+
trackAnimatedViewUpdate(viewTag, props)
|
|
146
|
+
}
|
|
147
|
+
2 -> { // INSTRUCTION_CREATE
|
|
148
|
+
val viewTag = intBuffer[i++]
|
|
149
|
+
// Mark as mounted
|
|
150
|
+
val fabricUIManager = UIManagerHelper.getUIManager(reactContext, UIManagerType.FABRIC)
|
|
151
|
+
val view = getViewByTag(fabricUIManager as Any, viewTag)
|
|
152
|
+
view?.let { ViewLifecycleRegistry.markMounted(it) }
|
|
153
|
+
}
|
|
154
|
+
8 -> { // INSTRUCTION_INSERT
|
|
155
|
+
val parentTag = intBuffer[i++]
|
|
156
|
+
val viewTag = intBuffer[i++]
|
|
157
|
+
val index = intBuffer[i++]
|
|
158
|
+
// Mark as mounted
|
|
159
|
+
val fabricUIManager = UIManagerHelper.getUIManager(reactContext, UIManagerType.FABRIC)
|
|
160
|
+
val view = getViewByTag(fabricUIManager as Any, viewTag)
|
|
161
|
+
view?.let { ViewLifecycleRegistry.markMounted(it) }
|
|
162
|
+
}
|
|
163
|
+
16 -> { // INSTRUCTION_REMOVE
|
|
164
|
+
val parentTag = intBuffer[i++]
|
|
165
|
+
val viewTag = intBuffer[i++]
|
|
166
|
+
val index = intBuffer[i++]
|
|
167
|
+
// View is being removed, no need to track
|
|
168
|
+
}
|
|
169
|
+
128 -> { // INSTRUCTION_UPDATE_LAYOUT
|
|
170
|
+
val viewTag = intBuffer[i++]
|
|
171
|
+
// Mark as updated
|
|
172
|
+
val fabricUIManager = UIManagerHelper.getUIManager(reactContext, UIManagerType.FABRIC)
|
|
173
|
+
val view = getViewByTag(fabricUIManager as Any, viewTag)
|
|
174
|
+
view?.let { ViewLifecycleRegistry.markUpdated(it) }
|
|
175
|
+
}
|
|
176
|
+
// Skip other instruction types
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} catch (e: Exception) {
|
|
180
|
+
// Silently ignore errors to avoid breaking the idling resource
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Track CreateMountItem for view creation.
|
|
186
|
+
*/
|
|
187
|
+
private fun trackCreateMountItem(mountItem: Any) {
|
|
188
|
+
try {
|
|
189
|
+
val viewTag = Reflect.on(mountItem).field("mReactTag").get<Int>()
|
|
190
|
+
// We can't get the actual View here, but we can track the tag
|
|
191
|
+
// The actual View will be available when it's mounted
|
|
192
|
+
} catch (e: Exception) {
|
|
193
|
+
// Silently ignore errors
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Track animated view update.
|
|
199
|
+
*/
|
|
200
|
+
private fun trackAnimatedViewUpdate(viewTag: Int, props: com.facebook.react.bridge.ReadableMap?) {
|
|
201
|
+
try {
|
|
202
|
+
// Get the actual Android View
|
|
203
|
+
val fabricUIManager = UIManagerHelper.getUIManager(reactContext, UIManagerType.FABRIC)
|
|
204
|
+
val view = getViewByTag(fabricUIManager as Any, viewTag)
|
|
205
|
+
|
|
206
|
+
if (view != null) {
|
|
207
|
+
com.wix.detox.inquiry.DetoxFabricAnimationHook.hookSynchronouslyUpdateViewOnUIThread(viewTag, props, fabricUIManager as com.facebook.react.fabric.FabricUIManager)
|
|
208
|
+
}
|
|
209
|
+
} catch (e: Exception) {
|
|
210
|
+
// Silently ignore errors to avoid breaking the idling resource
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get Android View by React Native view tag using correct Fabric APIs.
|
|
216
|
+
*/
|
|
217
|
+
private fun getViewByTag(fabricUIManager: Any, viewTag: Int): android.view.View? {
|
|
218
|
+
return try {
|
|
219
|
+
// Get MountingManager from FabricUIManager
|
|
220
|
+
val mountingManager = Reflect.on(fabricUIManager).field("mMountingManager").get<Any>()
|
|
221
|
+
|
|
222
|
+
// Get SurfaceMountingManager for the view
|
|
223
|
+
val getSurfaceManagerMethod = mountingManager.javaClass.getMethod("getSurfaceManagerForView", Int::class.java)
|
|
224
|
+
val surfaceMountingManager = getSurfaceManagerMethod.invoke(mountingManager, viewTag)
|
|
225
|
+
|
|
226
|
+
if (surfaceMountingManager != null) {
|
|
227
|
+
// Get the actual Android View
|
|
228
|
+
val getViewMethod = surfaceMountingManager.javaClass.getMethod("getView", Int::class.java)
|
|
229
|
+
getViewMethod.invoke(surfaceMountingManager, viewTag) as? android.view.View
|
|
230
|
+
} else {
|
|
231
|
+
null
|
|
232
|
+
}
|
|
233
|
+
} catch (e: Exception) {
|
|
234
|
+
null
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
70
239
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "detox",
|
|
3
3
|
"description": "E2E tests and automation for mobile",
|
|
4
|
-
"version": "20.
|
|
4
|
+
"version": "20.44.0-smoke.1",
|
|
5
5
|
"bin": {
|
|
6
6
|
"detox": "local-cli/cli.js"
|
|
7
7
|
},
|
|
@@ -120,5 +120,5 @@
|
|
|
120
120
|
"browserslist": [
|
|
121
121
|
"node 14"
|
|
122
122
|
],
|
|
123
|
-
"gitHead": "
|
|
123
|
+
"gitHead": "5e0b38a97eccb64d73369b1a3ccbd9c31ecde284"
|
|
124
124
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
016ffb2e4c490e01beafd1146a3a8353
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
097c3dad67f1dea0e2cdf756732aff413940b2c3
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
db8f2eb0cd02d2373018228e130132dc7d206625fcc2d7a54e6d9fa5a2d1f2e0
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
011ea72685f18e86f4ab86048a93fa47ff5e213bfcc83b9c7eb3fec558668c895f09479a083cca75fa0e0b4306e7ac808db22d2dbdb16f7732bfd03df2a7f929
|
|
Binary file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
c9638ce7992b4b6dfe4f83030aef5169
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
a2ed68ce4b428750daf9a3542447273ee1c171d3
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
52c115c682f497fb9925388a3e3b607e6c8a2742a71547e2aeca2457c048c6c6
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1ff3bc63cd2de2916880bec7572f7fd48612955d6192a094af89edc97daa88c03b7d646999dda185900875337de07898e523e96067e284fd1137ce35c1c18baa
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
17c5883f432236d65c08fef7086af8b6
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
06dfe524847a88faedbf0360272b4aaff6be4da0
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
5394023ab0482b86513f82bfb0072233bb04d5b809a05d804085a2cd59c677a5
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
8433d0f377655a489d92691746233edc865b1040318b708f0a451aade0254bda5989a7bcd042ffe8ecd3e65fc636b0e945a58c86d616b590e6c24ed8974402a1
|