detox 20.43.0 → 20.44.0-smoke.0
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.0/detox-20.44.0-smoke.0-sources.jar} +0 -0
- package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0-sources.jar.md5 +1 -0
- package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0-sources.jar.sha1 +1 -0
- package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0-sources.jar.sha256 +1 -0
- package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0-sources.jar.sha512 +1 -0
- package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0.aar +0 -0
- package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0.aar.md5 +1 -0
- package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0.aar.sha1 +1 -0
- package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0.aar.sha256 +1 -0
- package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0.aar.sha512 +1 -0
- package/Detox-android/com/wix/detox/{20.43.0/detox-20.43.0.pom → 20.44.0-smoke.0/detox-20.44.0-smoke.0.pom} +1 -1
- package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0.pom.md5 +1 -0
- package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0.pom.sha1 +1 -0
- package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0.pom.sha256 +1 -0
- package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0.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/DetoxFabricAnimationHook.kt +192 -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 +207 -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
|
+
a3db6cd0603cac8288674ad9a3500d8f
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1ad7d900ea972d2ee93f237abcc969e0ad522652
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
b0290a59c47efb8a80702a11a2dfb69498ddcd0da14d63d30f558574105fbe17
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
81bcbcbc24ded6f8b469f8be58ad0195f72b39faadc1f3da271d646c93756dac15476d208b1424cde7f9966614a01e122452083b060094bde60ba64d080ff9c4
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
216f0dfc73c1b4f9ab0514607d5f5f2b
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
9d8dfd8b0ac4ebeaeb7f4af1d8ab1d650046662d
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
7c48b2a45b2deda2526d564ffc1472a9ec7dd5d123591113106b480281c5e9ce
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4208bb610d9268ad130dba46fa3a08eb2dd39660b700b3218c3b784937b8248ff93c51443bce4cff4d339bf3cc8969f37159235d26b284b7accb00e1ca30722f
|
|
@@ -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.0</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
|
+
a4be7bd3d45ebb560642a7c0761625fd
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
285ed616044919523ccdcc037a962672de13646e
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0338db52bbfeb69095f0a1c31abb80acec835b1031e60a1047982260ab232ff8
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
b2bf4d188dfbeca23574936656a9e09936762f5d53263529414696aa294770c324d556d309a1e9887b1e56734eb706072ab4cfc880af4bdbfe9e9d9b26618930
|
|
@@ -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.0</latest>
|
|
7
|
+
<release>20.44.0-smoke.0</release>
|
|
8
8
|
<versions>
|
|
9
|
-
<version>20.
|
|
9
|
+
<version>20.44.0-smoke.0</version>
|
|
10
10
|
</versions>
|
|
11
|
-
<lastUpdated>
|
|
11
|
+
<lastUpdated>20251009064730</lastUpdated>
|
|
12
12
|
</versioning>
|
|
13
13
|
</metadata>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
6fd136d7a8c19e7c19f3c34acafd3f68
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
3925c59668fe2612ba6a696b291a0ca11553fbbe
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
0c09abf9ea6b645efa11a1b3a06900511d2bb8c7e941c5e4ee15ea7babd94278
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
eb554b0043657d82361a9955b128c1fe514454844892b7daffef616c59ae39c3de434033fc6dcf1f7521422ef7201739b7b01af7ba6a8f73b4beca11073a7630
|
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,192 @@
|
|
|
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.mounting.SurfaceMountingManager
|
|
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
|
+
import com.wix.detox.inquiry.ViewLifecycleRegistry.markCustomEvent
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Hook into React Native's Fabric new architecture to track view lifecycle events.
|
|
14
|
+
* This hooks into the exact points where views are mounted, updated, and animated.
|
|
15
|
+
*/
|
|
16
|
+
object DetoxFabricAnimationHook {
|
|
17
|
+
private const val LOG_TAG = "DetoxFabricHook"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Hook into IntBufferBatchMountItem.execute() to track animated view updates.
|
|
21
|
+
* This is called when animated props are applied to views in Fabric.
|
|
22
|
+
*/
|
|
23
|
+
fun hookIntBufferBatchMountItem(
|
|
24
|
+
viewTag: Int,
|
|
25
|
+
props: ReadableMap?,
|
|
26
|
+
surfaceMountingManager: SurfaceMountingManager
|
|
27
|
+
) {
|
|
28
|
+
try {
|
|
29
|
+
// Get the actual Android View
|
|
30
|
+
val androidView = getViewByTag(surfaceMountingManager, viewTag)
|
|
31
|
+
if (androidView == null) {
|
|
32
|
+
Log.d(LOG_TAG, "View not found for tag: $viewTag")
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check if this is an animated update
|
|
37
|
+
if (isAnimatedPropsUpdate(props)) {
|
|
38
|
+
Log.d(LOG_TAG, "Animated props update for view tag: $viewTag")
|
|
39
|
+
markAnimated(androidView)
|
|
40
|
+
|
|
41
|
+
// Log problematic animations
|
|
42
|
+
if (isProblematicAnimation(props)) {
|
|
43
|
+
Log.w(LOG_TAG, "Problematic animation detected for view tag: $viewTag")
|
|
44
|
+
markCustomEvent(androidView, "problematic_animation")
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
// Regular props update
|
|
48
|
+
markUpdated(androidView)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
} catch (e: Exception) {
|
|
52
|
+
Log.w(LOG_TAG, "Failed to hook animated view update", e)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Hook into view mount operations to track when views are created.
|
|
58
|
+
*/
|
|
59
|
+
fun hookViewMount(
|
|
60
|
+
viewTag: Int,
|
|
61
|
+
surfaceMountingManager: SurfaceMountingManager
|
|
62
|
+
) {
|
|
63
|
+
try {
|
|
64
|
+
val androidView = getViewByTag(surfaceMountingManager, viewTag)
|
|
65
|
+
if (androidView != null) {
|
|
66
|
+
Log.d(LOG_TAG, "View mounted with tag: $viewTag")
|
|
67
|
+
markMounted(androidView)
|
|
68
|
+
}
|
|
69
|
+
} catch (e: Exception) {
|
|
70
|
+
Log.w(LOG_TAG, "Failed to hook view mount", e)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get Android View by React Native view tag using reflection.
|
|
76
|
+
* This works around the fact that SurfaceMountingManager doesn't expose a direct getView method.
|
|
77
|
+
*/
|
|
78
|
+
private fun getViewByTag(
|
|
79
|
+
surfaceMountingManager: SurfaceMountingManager,
|
|
80
|
+
viewTag: Int
|
|
81
|
+
): View? {
|
|
82
|
+
return try {
|
|
83
|
+
// Use reflection to access the internal view registry
|
|
84
|
+
val viewRegistryField = surfaceMountingManager.javaClass.getDeclaredField("mViewRegistry")
|
|
85
|
+
viewRegistryField.isAccessible = true
|
|
86
|
+
val viewRegistry = viewRegistryField.get(surfaceMountingManager)
|
|
87
|
+
|
|
88
|
+
// Get the view from the registry
|
|
89
|
+
val getViewMethod = viewRegistry.javaClass.getMethod("getView", Int::class.java)
|
|
90
|
+
getViewMethod.invoke(viewRegistry, viewTag) as? View
|
|
91
|
+
} catch (e: Exception) {
|
|
92
|
+
Log.w(LOG_TAG, "Failed to get view by tag: $viewTag", e)
|
|
93
|
+
null
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if this is an animated props update by looking for animated properties.
|
|
99
|
+
*/
|
|
100
|
+
private fun isAnimatedPropsUpdate(props: ReadableMap?): Boolean {
|
|
101
|
+
if (props == null) return false
|
|
102
|
+
|
|
103
|
+
val animatedKeys = setOf(
|
|
104
|
+
"transform", "opacity", "scaleX", "scaleY", "scale",
|
|
105
|
+
"translateX", "translateY", "rotateX", "rotateY", "rotateZ",
|
|
106
|
+
"backgroundColor", "borderRadius", "borderWidth"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
val iterator = props.keySetIterator()
|
|
110
|
+
while (iterator.hasNextKey()) {
|
|
111
|
+
val key = iterator.nextKey()
|
|
112
|
+
if (animatedKeys.any { key.contains(it, ignoreCase = true) }) {
|
|
113
|
+
return true
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if this animation might be problematic (infinite loops, conflicting animations, etc.).
|
|
122
|
+
*/
|
|
123
|
+
private fun isProblematicAnimation(props: ReadableMap?): Boolean {
|
|
124
|
+
if (props == null) return false
|
|
125
|
+
|
|
126
|
+
// Check for potential infinite loop patterns
|
|
127
|
+
val transformKeys = mutableSetOf<String>()
|
|
128
|
+
val iterator = props.keySetIterator()
|
|
129
|
+
|
|
130
|
+
while (iterator.hasNextKey()) {
|
|
131
|
+
val key = iterator.nextKey()
|
|
132
|
+
if (key.contains("transform", ignoreCase = true)) {
|
|
133
|
+
transformKeys.add(key)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Multiple transform properties might indicate conflicting animations
|
|
138
|
+
if (transformKeys.size > 3) {
|
|
139
|
+
Log.w(LOG_TAG, "Multiple transform properties detected: $transformKeys")
|
|
140
|
+
return true
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check for opacity animations that might cause issues
|
|
144
|
+
if (props.hasKey("opacity")) {
|
|
145
|
+
val opacity = props.getDouble("opacity")
|
|
146
|
+
if (opacity < 0.0 || opacity > 1.0) {
|
|
147
|
+
Log.w(LOG_TAG, "Invalid opacity value: $opacity")
|
|
148
|
+
return true
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return false
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get view coordinates for highlighting
|
|
157
|
+
*/
|
|
158
|
+
fun getViewCoordinates(view: View): IntArray {
|
|
159
|
+
val coords = intArrayOf(0, 0, 0, 0)
|
|
160
|
+
try {
|
|
161
|
+
view.getLocationOnScreen(coords)
|
|
162
|
+
coords[2] = view.width
|
|
163
|
+
coords[3] = view.height
|
|
164
|
+
} catch (e: Exception) {
|
|
165
|
+
Log.w(LOG_TAG, "Failed to get view coordinates", e)
|
|
166
|
+
}
|
|
167
|
+
return coords
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get view coordinates relative to root view
|
|
172
|
+
*/
|
|
173
|
+
fun getViewCoordinatesRelativeToRoot(view: View, rootView: View): IntArray {
|
|
174
|
+
val viewCoords = getViewCoordinates(view)
|
|
175
|
+
val rootCoords = getViewCoordinates(rootView)
|
|
176
|
+
|
|
177
|
+
return intArrayOf(
|
|
178
|
+
viewCoords[0] - rootCoords[0], // Relative X
|
|
179
|
+
viewCoords[1] - rootCoords[1], // Relative Y
|
|
180
|
+
viewCoords[2], // Width
|
|
181
|
+
viewCoords[3] // Height
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Log current registry statistics
|
|
187
|
+
*/
|
|
188
|
+
fun logRegistryStats() {
|
|
189
|
+
val stats = ViewLifecycleRegistry.getStats()
|
|
190
|
+
Log.i(LOG_TAG, "ViewLifecycleRegistry stats: $stats")
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -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,203 @@ 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
|
+
// Check if this is an animated update
|
|
208
|
+
val isAnimated = isAnimatedPropsUpdate(props)
|
|
209
|
+
|
|
210
|
+
if (isAnimated) {
|
|
211
|
+
ViewLifecycleRegistry.markAnimated(view)
|
|
212
|
+
} else {
|
|
213
|
+
ViewLifecycleRegistry.markUpdated(view)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch (e: Exception) {
|
|
217
|
+
// Silently ignore errors to avoid breaking the idling resource
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get Android View by React Native view tag using correct Fabric APIs.
|
|
223
|
+
*/
|
|
224
|
+
private fun getViewByTag(fabricUIManager: Any, viewTag: Int): android.view.View? {
|
|
225
|
+
return try {
|
|
226
|
+
// Get MountingManager from FabricUIManager
|
|
227
|
+
val mountingManager = Reflect.on(fabricUIManager).field("mMountingManager").get<Any>()
|
|
228
|
+
|
|
229
|
+
// Get SurfaceMountingManager for the view
|
|
230
|
+
val getSurfaceManagerMethod = mountingManager.javaClass.getMethod("getSurfaceManagerForView", Int::class.java)
|
|
231
|
+
val surfaceMountingManager = getSurfaceManagerMethod.invoke(mountingManager, viewTag)
|
|
232
|
+
|
|
233
|
+
if (surfaceMountingManager != null) {
|
|
234
|
+
// Get the actual Android View
|
|
235
|
+
val getViewMethod = surfaceMountingManager.javaClass.getMethod("getView", Int::class.java)
|
|
236
|
+
getViewMethod.invoke(surfaceMountingManager, viewTag) as? android.view.View
|
|
237
|
+
} else {
|
|
238
|
+
null
|
|
239
|
+
}
|
|
240
|
+
} catch (e: Exception) {
|
|
241
|
+
null
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Check if this is an animated props update.
|
|
247
|
+
*/
|
|
248
|
+
private fun isAnimatedPropsUpdate(props: com.facebook.react.bridge.ReadableMap?): Boolean {
|
|
249
|
+
if (props == null) {
|
|
250
|
+
Log.i("DetoxFabricDebug", "Props is null - not animated")
|
|
251
|
+
return false
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
val animatedKeys = setOf(
|
|
255
|
+
"transform", "opacity", "scaleX", "scaleY", "scale",
|
|
256
|
+
"translateX", "translateY", "rotateX", "rotateY", "rotateZ",
|
|
257
|
+
"backgroundColor", "borderRadius", "borderWidth"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
Log.i("DetoxFabricDebug", "Checking props for animated keys...")
|
|
261
|
+
|
|
262
|
+
val iterator = props.keySetIterator()
|
|
263
|
+
while (iterator.hasNextKey()) {
|
|
264
|
+
val key = iterator.nextKey()
|
|
265
|
+
Log.i("DetoxFabricDebug", "Checking key: $key")
|
|
266
|
+
if (animatedKeys.any { key.contains(it, ignoreCase = true) }) {
|
|
267
|
+
Log.i("DetoxFabricDebug", "Found animated key: $key")
|
|
268
|
+
return true
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
Log.i("DetoxFabricDebug", "No animated keys found")
|
|
273
|
+
return false
|
|
274
|
+
}
|
|
275
|
+
|
|
70
276
|
}
|
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.0",
|
|
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": "3a5520e1a0fcb8f727cd58f43389be5482302224"
|
|
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
|