detox 20.43.0-alpha.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.
Files changed (44) hide show
  1. package/Detox-android/com/wix/detox/{20.43.0-alpha.0/detox-20.43.0-alpha.0-sources.jar → 20.44.0-smoke.0/detox-20.44.0-smoke.0-sources.jar} +0 -0
  2. package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0-sources.jar.md5 +1 -0
  3. package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0-sources.jar.sha1 +1 -0
  4. package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0-sources.jar.sha256 +1 -0
  5. package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0-sources.jar.sha512 +1 -0
  6. package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0.aar +0 -0
  7. package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0.aar.md5 +1 -0
  8. package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0.aar.sha1 +1 -0
  9. package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0.aar.sha256 +1 -0
  10. package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0.aar.sha512 +1 -0
  11. package/Detox-android/com/wix/detox/{20.43.0-alpha.0/detox-20.43.0-alpha.0.pom → 20.44.0-smoke.0/detox-20.44.0-smoke.0.pom} +1 -1
  12. package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0.pom.md5 +1 -0
  13. package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0.pom.sha1 +1 -0
  14. package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0.pom.sha256 +1 -0
  15. package/Detox-android/com/wix/detox/20.44.0-smoke.0/detox-20.44.0-smoke.0.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/DetoxFabricAnimationHook.kt +192 -0
  27. package/android/detox/src/full/java/com/wix/detox/inquiry/ViewLifecycleRegistry.kt +233 -0
  28. package/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/network/NetworkingModuleReflected.kt +4 -4
  29. package/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/timers/JavaTimersReflected.kt +4 -5
  30. package/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/fabric/FabricUIManagerIdlingResources.kt +207 -1
  31. package/package.json +2 -2
  32. package/Detox-android/com/wix/detox/20.43.0-alpha.0/detox-20.43.0-alpha.0-sources.jar.md5 +0 -1
  33. package/Detox-android/com/wix/detox/20.43.0-alpha.0/detox-20.43.0-alpha.0-sources.jar.sha1 +0 -1
  34. package/Detox-android/com/wix/detox/20.43.0-alpha.0/detox-20.43.0-alpha.0-sources.jar.sha256 +0 -1
  35. package/Detox-android/com/wix/detox/20.43.0-alpha.0/detox-20.43.0-alpha.0-sources.jar.sha512 +0 -1
  36. package/Detox-android/com/wix/detox/20.43.0-alpha.0/detox-20.43.0-alpha.0.aar +0 -0
  37. package/Detox-android/com/wix/detox/20.43.0-alpha.0/detox-20.43.0-alpha.0.aar.md5 +0 -1
  38. package/Detox-android/com/wix/detox/20.43.0-alpha.0/detox-20.43.0-alpha.0.aar.sha1 +0 -1
  39. package/Detox-android/com/wix/detox/20.43.0-alpha.0/detox-20.43.0-alpha.0.aar.sha256 +0 -1
  40. package/Detox-android/com/wix/detox/20.43.0-alpha.0/detox-20.43.0-alpha.0.aar.sha512 +0 -1
  41. package/Detox-android/com/wix/detox/20.43.0-alpha.0/detox-20.43.0-alpha.0.pom.md5 +0 -1
  42. package/Detox-android/com/wix/detox/20.43.0-alpha.0/detox-20.43.0-alpha.0.pom.sha1 +0 -1
  43. package/Detox-android/com/wix/detox/20.43.0-alpha.0/detox-20.43.0-alpha.0.pom.sha256 +0 -1
  44. package/Detox-android/com/wix/detox/20.43.0-alpha.0/detox-20.43.0-alpha.0.pom.sha512 +0 -1
@@ -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.43.0-alpha.0</version>
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.43.0-alpha.0</latest>
7
- <release>20.43.0-alpha.0</release>
6
+ <latest>20.44.0-smoke.0</latest>
7
+ <release>20.44.0-smoke.0</release>
8
8
  <versions>
9
- <version>20.43.0-alpha.0</version>
9
+ <version>20.44.0-smoke.0</version>
10
10
  </versions>
11
- <lastUpdated>20250930221159</lastUpdated>
11
+ <lastUpdated>20251009064730</lastUpdated>
12
12
  </versioning>
13
13
  </metadata>
@@ -1 +1 @@
1
- 12c4b723cd5d9608e020fdc70c366fed
1
+ 6fd136d7a8c19e7c19f3c34acafd3f68
@@ -1 +1 @@
1
- 6dbaf00aa421e38bb3c590da262763ef7b4b00c0
1
+ 3925c59668fe2612ba6a696b291a0ca11553fbbe
@@ -1 +1 @@
1
- b69af31d15bc2fa243024937c8336091f0efbd3622cddb163764c1e10b503c5b
1
+ 0c09abf9ea6b645efa11a1b3a06900511d2bb8c7e941c5e4ee15ea7babd94278
@@ -1 +1 @@
1
- 227d31b3252ef18d07f89baee17eec925668e555796e6e2aec484d34be8af38c4a23962a2b2bbc1d2be99b1a19beccabd71d94f2cdddceb26175b26cf519bcd6
1
+ eb554b0043657d82361a9955b128c1fe514454844892b7daffef616c59ae39c3de434033fc6dcf1f7521422ef7201739b7b01af7ba6a8f73b4beca11073a7630
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,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
+ }
@@ -11,20 +11,20 @@ import org.joor.ReflectException
11
11
 
12
12
  private const val LOG_TAG = "RNNetworkingModuleRefl"
13
13
 
14
- private const val FIELD_OKHTTP_CLIENT_RN79 = "mClient"
14
+ private const val FIELD_OKHTTP_CLIENT_PRE80 = "mClient"
15
15
  private const val FIELD_OKHTTP_CLIENT = "client"
16
16
 
17
17
  internal class NetworkingModuleReflected(private val reactContext: ReactContext) {
18
18
  fun getHttpClient(): OkHttpClient? {
19
19
  val networkNativeModule = reactContext.getNativeModule(NetworkingModule::class.java)
20
20
  try {
21
- val filedName = if ( ReactNativeInfo.rnVersion().minor > 79) {
21
+ val fieldName = if ( ReactNativeInfo.rnVersion().minor > 79) {
22
22
  FIELD_OKHTTP_CLIENT
23
23
  } else {
24
- FIELD_OKHTTP_CLIENT_RN79
24
+ FIELD_OKHTTP_CLIENT_PRE80
25
25
  }
26
26
 
27
- return Reflect.on(networkNativeModule).field(filedName).get()
27
+ return Reflect.on(networkNativeModule).field(fieldName).get()
28
28
  } catch (e: ReflectException) {
29
29
  Log.e(LOG_TAG, "Can't set up Networking Module listener", e)
30
30
  return null
@@ -1,6 +1,5 @@
1
1
  package com.wix.detox.reactnative.idlingresources.timers
2
2
 
3
- import android.os.Debug
4
3
  import com.facebook.react.bridge.ReactContext
5
4
  import com.facebook.react.modules.core.JavaTimerManager
6
5
  import com.wix.detox.reactnative.ReactNativeInfo
@@ -21,19 +20,19 @@ object JavaTimersReflected {
21
20
  }
22
21
 
23
22
  private fun getTimersManager(reactContext: ReactContext): JavaTimerManager {
24
- val reactHostFiledName = if (ReactNativeInfo.rnVersion().minor > 79) {
23
+ val reactHostFieldName = if (ReactNativeInfo.rnVersion().minor > 79) {
25
24
  "reactHost"
26
25
  } else {
27
26
  "mReactHost"
28
27
  }
29
- val javaTimerManagerFiledName = if (ReactNativeInfo.rnVersion().minor > 79) {
28
+ val javaTimerManagerFieldName = if (ReactNativeInfo.rnVersion().minor > 79) {
30
29
  "javaTimerManager"
31
30
  } else {
32
31
  "mJavaTimerManager"
33
32
  }
34
33
 
35
- val reactHost = Reflect.on(reactContext).field(reactHostFiledName).get<Any>()
34
+ val reactHost = Reflect.on(reactContext).field(reactHostFieldName).get<Any>()
36
35
  val reactInstance = Reflect.on(reactHost).field("mReactInstance").get<Any>()
37
- return Reflect.on(reactInstance).field(javaTimerManagerFiledName).get() as JavaTimerManager
36
+ return Reflect.on(reactInstance).field(javaTimerManagerFieldName).get() as JavaTimerManager
38
37
  }
39
38
  }
@@ -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,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.43.0-alpha.0",
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": "35529102f5a90db7a56f1ecdd7d834cceac9061d"
123
+ "gitHead": "3a5520e1a0fcb8f727cd58f43389be5482302224"
124
124
  }
@@ -1 +0,0 @@
1
- b02764fe99571d429f7c190b87219c69
@@ -1 +0,0 @@
1
- e4bdc6e9ec26c6520abd05a73d758c4c48eaef18
@@ -1 +0,0 @@
1
- 17766f2604820bb14066ffda5b5a56eca73bf5e05d8eabb784a6af17dc34468f
@@ -1 +0,0 @@
1
- 18da540136022798c270cfc57324a967f14f1d76c1188cc9ebbf84190e2c68af04b2dc0c13642ef8846a0ac2da38c9ec4d6ee8a99e77499b4c8b821abd7567aa
@@ -1 +0,0 @@
1
- 95f859e3353c02c1bdfae62eecdb7064
@@ -1 +0,0 @@
1
- e849c7c098b6fc1349725b02130c0940ff2671d6
@@ -1 +0,0 @@
1
- 28933f70ef1c9e5312509ff18562b792c8e4d620f877953851b071e2cdc38c2c
@@ -1 +0,0 @@
1
- 76c60df97a57e87f1e3722927c66e5e698c4f42699392ca74cec18f8c97a2330b30ab335c70df3557ae98a8efbddd2865591430744294de7842eb3012dc936c0
@@ -1 +0,0 @@
1
- 45a3353f06c37299757d2b897327ff46
@@ -1 +0,0 @@
1
- a16120462fe2e18597a680d505b2f207b86a59f4
@@ -1 +0,0 @@
1
- 7139e0634426ad1772df186bf9589f59412b869132d48358807c808d7612e2e1
@@ -1 +0,0 @@
1
- f6ee060d3e172960674cf8cb65bd213a853ea93703b9d231d992feead0f24d03c49bd8c49abbb41decc0f50c81aa7c746ac2101d93bd0f96f2750cae79dc3cc3