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.
Files changed (45) hide show
  1. 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
  2. package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1-sources.jar.md5 +1 -0
  3. package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1-sources.jar.sha1 +1 -0
  4. package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1-sources.jar.sha256 +1 -0
  5. package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1-sources.jar.sha512 +1 -0
  6. package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1.aar +0 -0
  7. package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1.aar.md5 +1 -0
  8. package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1.aar.sha1 +1 -0
  9. package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1.aar.sha256 +1 -0
  10. package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1.aar.sha512 +1 -0
  11. 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
  12. package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1.pom.md5 +1 -0
  13. package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1.pom.sha1 +1 -0
  14. package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1.pom.sha256 +1 -0
  15. package/Detox-android/com/wix/detox/20.44.0-smoke.1/detox-20.44.0-smoke.1.pom.sha512 +1 -0
  16. package/Detox-android/com/wix/detox/maven-metadata.xml +4 -4
  17. package/Detox-android/com/wix/detox/maven-metadata.xml.md5 +1 -1
  18. package/Detox-android/com/wix/detox/maven-metadata.xml.sha1 +1 -1
  19. package/Detox-android/com/wix/detox/maven-metadata.xml.sha256 +1 -1
  20. package/Detox-android/com/wix/detox/maven-metadata.xml.sha512 +1 -1
  21. package/Detox-ios-framework.tbz +0 -0
  22. package/Detox-ios-src.tbz +0 -0
  23. package/Detox-ios-xcuitest.tbz +0 -0
  24. package/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionHandlers.kt +7 -1
  25. package/android/detox/src/full/java/com/wix/detox/espresso/hierarchy/ViewHierarchyGenerator.kt +64 -0
  26. package/android/detox/src/full/java/com/wix/detox/inquiry/DetoxAnimationTracker.kt +70 -0
  27. package/android/detox/src/full/java/com/wix/detox/inquiry/DetoxFabricAnimationHook.kt +83 -0
  28. package/android/detox/src/full/java/com/wix/detox/inquiry/DetoxFabricIntegration.kt +99 -0
  29. package/android/detox/src/full/java/com/wix/detox/inquiry/DetoxFabricUIManagerWrapper.kt +37 -0
  30. package/android/detox/src/full/java/com/wix/detox/inquiry/ViewLifecycleRegistry.kt +233 -0
  31. package/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/fabric/FabricUIManagerIdlingResources.kt +170 -1
  32. package/package.json +2 -2
  33. package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0-sources.jar.md5 +0 -1
  34. package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0-sources.jar.sha1 +0 -1
  35. package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0-sources.jar.sha256 +0 -1
  36. package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0-sources.jar.sha512 +0 -1
  37. package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0.aar +0 -0
  38. package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0.aar.md5 +0 -1
  39. package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0.aar.sha1 +0 -1
  40. package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0.aar.sha256 +0 -1
  41. package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0.aar.sha512 +0 -1
  42. package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0.pom.md5 +0 -1
  43. package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0.pom.sha1 +0 -1
  44. package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0.pom.sha256 +0 -1
  45. package/Detox-android/com/wix/detox/20.43.0/detox-20.43.0.pom.sha512 +0 -1
@@ -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.43.0</version>
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.43.0</latest>
7
- <release>20.43.0</release>
6
+ <latest>20.44.0-smoke.1</latest>
7
+ <release>20.44.0-smoke.1</release>
8
8
  <versions>
9
- <version>20.43.0</version>
9
+ <version>20.44.0-smoke.1</version>
10
10
  </versions>
11
- <lastUpdated>20251005174840</lastUpdated>
11
+ <lastUpdated>20251009104839</lastUpdated>
12
12
  </versioning>
13
13
  </metadata>
@@ -1 +1 @@
1
- 886f630a5f15d5956dc1ee88ddc28a93
1
+ 4baad9ede6680e5d2b46f1246a48c8f9
@@ -1 +1 @@
1
- 9827172d9c41c9c0b8aebcafed3c6aa9fdee38f3
1
+ b07f36a5c0ae07b6a7961b318ca15b8acc44d602
@@ -1 +1 @@
1
- f72c0489fa076f46a4b8f1f067a2d61d92d8b1e6b803abd1a7497197da9bdf23
1
+ a95b41be51ad53267864aae6fc777db00648abe236c7bdbd46c645543e9c052f
@@ -1 +1 @@
1
- cbaade5e87589c6fb562c9434649cf4097519e46df99f89d5f18169b1bdc623afbced20c9f2631110773d7c35e2751358669f23b7aef6949feb01b035468686e
1
+ 03f466d7a8a4a9f3a5b82b78cc9fcfc0020dd9bb7e3c7d821096887eb241118db40ce09f2f4912a6de05bd2b4c4d75ad48133665e37f68c37bae4a3b227d08ca
Binary file
package/Detox-ios-src.tbz CHANGED
Binary file
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
- null
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(
@@ -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
- return if (getViewCommandMountItemsSize() == 0 && getMountItemsSize() == 0) {
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.43.0",
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": "28c2bb5d49a78a0b7103ecd15a8d6a148b1f7d3d"
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
@@ -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