detox 20.44.0-smoke.4 → 20.44.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 (51) hide show
  1. package/Detox-android/com/wix/detox/{20.44.0-smoke.4/detox-20.44.0-smoke.4-sources.jar → 20.44.0/detox-20.44.0-sources.jar} +0 -0
  2. package/Detox-android/com/wix/detox/20.44.0/detox-20.44.0-sources.jar.md5 +1 -0
  3. package/Detox-android/com/wix/detox/20.44.0/detox-20.44.0-sources.jar.sha1 +1 -0
  4. package/Detox-android/com/wix/detox/20.44.0/detox-20.44.0-sources.jar.sha256 +1 -0
  5. package/Detox-android/com/wix/detox/20.44.0/detox-20.44.0-sources.jar.sha512 +1 -0
  6. package/Detox-android/com/wix/detox/20.44.0/detox-20.44.0.aar +0 -0
  7. package/Detox-android/com/wix/detox/20.44.0/detox-20.44.0.aar.md5 +1 -0
  8. package/Detox-android/com/wix/detox/20.44.0/detox-20.44.0.aar.sha1 +1 -0
  9. package/Detox-android/com/wix/detox/20.44.0/detox-20.44.0.aar.sha256 +1 -0
  10. package/Detox-android/com/wix/detox/20.44.0/detox-20.44.0.aar.sha512 +1 -0
  11. package/Detox-android/com/wix/detox/{20.44.0-smoke.4/detox-20.44.0-smoke.4.pom → 20.44.0/detox-20.44.0.pom} +1 -1
  12. package/Detox-android/com/wix/detox/20.44.0/detox-20.44.0.pom.md5 +1 -0
  13. package/Detox-android/com/wix/detox/20.44.0/detox-20.44.0.pom.sha1 +1 -0
  14. package/Detox-android/com/wix/detox/20.44.0/detox-20.44.0.pom.sha256 +1 -0
  15. package/Detox-android/com/wix/detox/20.44.0/detox-20.44.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/proguard-rules-app.pro +1 -0
  25. package/android/detox/src/full/java/com/wix/detox/common/KotlinReflectUtils.kt +31 -0
  26. package/android/detox/src/full/java/com/wix/detox/espresso/hierarchy/ViewHierarchyGenerator.kt +0 -15
  27. package/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/animations/AnimatedModuleIdlingResource.kt +3 -20
  28. package/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/timers/JavaTimersReflected.kt +8 -1
  29. package/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/fabric/FabricUIManagerIdlingResources.kt +14 -3
  30. package/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/paper/UIModuleIdlingResource.kt +1 -1
  31. package/android/detox/src/full/java/com/wix/detox/reactnative/ui/UIExtensions.kt +7 -3
  32. package/android/detox/src/testFull/java/com/wix/detox/common/UIExtensionsTest.kt +3 -1
  33. package/android/gradle/wrapper/gradle-wrapper.properties +1 -1
  34. package/android/rninfo.gradle +1 -0
  35. package/package.json +11 -11
  36. package/scripts/updateGradle.js +1 -1
  37. package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4-sources.jar.md5 +0 -1
  38. package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4-sources.jar.sha1 +0 -1
  39. package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4-sources.jar.sha256 +0 -1
  40. package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4-sources.jar.sha512 +0 -1
  41. package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4.aar +0 -0
  42. package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4.aar.md5 +0 -1
  43. package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4.aar.sha1 +0 -1
  44. package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4.aar.sha256 +0 -1
  45. package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4.aar.sha512 +0 -1
  46. package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4.pom.md5 +0 -1
  47. package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4.pom.sha1 +0 -1
  48. package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4.pom.sha256 +0 -1
  49. package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4.pom.sha512 +0 -1
  50. package/android/detox/src/full/java/com/wix/detox/inquiry/FabricAnimationsInquirer.kt +0 -629
  51. package/android/detox/src/full/java/com/wix/detox/inquiry/ViewLifecycleRegistry.kt +0 -143
@@ -0,0 +1 @@
1
+ 8742023668a8e532cf18692cad091938
@@ -0,0 +1 @@
1
+ ad0bb17a22af68dececf00e90f2b6ddb0faade67
@@ -0,0 +1 @@
1
+ cb9a4c6ad50b011b5ac72ffb30b66866af988d647bcffd645b3f38f589add79e
@@ -0,0 +1 @@
1
+ 532b3de4c23855a7bf62dbddf8406e5656bf81a3435a39adad9645fd019b679c0e16e699ce1e8085c9401dda8e8ffa661904e586f3c3897eddf7e143ae06dbbe
@@ -0,0 +1 @@
1
+ 288bdce8c1fccf5fcf9fc72e8b4ee44d
@@ -0,0 +1 @@
1
+ 93430b9679720bfa8934353c250fba1595144469
@@ -0,0 +1 @@
1
+ 843856839476df2363fc390f8fbf1a524099990b4ce2eba11b21ca9a77e34e1c
@@ -0,0 +1 @@
1
+ b56d42e400cb99d711c73de989b12b3fb7dcdf369929ff0ffdd5539fd5cbf299657c3aa7e75991ee0b4b6198c0792ec5fc7f671b3dbb7381f6fbf6e4d94afde1
@@ -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.44.0-smoke.4</version>
6
+ <version>20.44.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
+ 7760c7533c59d591c05ba1f3b13b8b4a
@@ -0,0 +1 @@
1
+ c39734d13b51edebee43ca52381e1e85a8a80878
@@ -0,0 +1 @@
1
+ 48ab199f2609d9fedac60f29efe20b95a797f380fa116e6e29e22f7d3f9fde6a
@@ -0,0 +1 @@
1
+ ce99db5cea5d30288834e7209bd6dfcfe63301ac6d992db7fd39dca4145741db5839f20dbbed53b198711cf79ca5d83f1efea28f03173205173181ad219cdcc8
@@ -3,11 +3,11 @@
3
3
  <groupId>com.wix</groupId>
4
4
  <artifactId>detox</artifactId>
5
5
  <versioning>
6
- <latest>20.44.0-smoke.4</latest>
7
- <release>20.44.0-smoke.4</release>
6
+ <latest>20.44.0</latest>
7
+ <release>20.44.0</release>
8
8
  <versions>
9
- <version>20.44.0-smoke.4</version>
9
+ <version>20.44.0</version>
10
10
  </versions>
11
- <lastUpdated>20251015123718</lastUpdated>
11
+ <lastUpdated>20251019160654</lastUpdated>
12
12
  </versioning>
13
13
  </metadata>
@@ -1 +1 @@
1
- 1424a26ea132414e2a25cdf24897ca55
1
+ 129e8845e2c49ebb6a8a08917ba2f5ca
@@ -1 +1 @@
1
- 419ebf124657f635cb3ecec2f6a3256cce093e46
1
+ 5155667329c4ec88cdebcd3eec1e00ab5cc18aab
@@ -1 +1 @@
1
- e31c759d7ed1f7bea2b6e77276bbb1eb2ef29291b9d2a81162dfb785de74816d
1
+ 3141cb84e424059e9ed45660a82052b581406ab15a4fc8c400df3c1950fae08c
@@ -1 +1 @@
1
- c4e0b8a52b3cb63613de5310d16ef65fbe591dede27d78812e55b19a527b00cc5521e4dcbf9ebe5331147e8c5b37dd0114a366255eacfb4e8788a27f999066f0
1
+ 81198b46c3596f99eb9a5ed2536ed0cc18594792109aec6e36e91bc4be60f5cc5750b98c4add46ecc407ac4502f7c7fb66313f710c673ddfcec10f13bcb99ac9
Binary file
package/Detox-ios-src.tbz CHANGED
Binary file
Binary file
@@ -43,6 +43,7 @@
43
43
  -keep class kotlin.LazyKt { *; }
44
44
 
45
45
  -keep class androidx.concurrent.futures.** { *; }
46
+ -keep class androidx.tracing.** { *; }
46
47
 
47
48
  -dontwarn androidx.appcompat.**
48
49
  -dontwarn javax.lang.model.element.**
@@ -0,0 +1,31 @@
1
+ @file:Suppress("UNCHECKED_CAST")
2
+
3
+ package com.wix.detox.common
4
+
5
+ import kotlin.reflect.full.memberFunctions
6
+ import kotlin.reflect.full.memberProperties
7
+ import kotlin.reflect.jvm.isAccessible
8
+
9
+ object KotlinReflectUtils {
10
+
11
+
12
+ /**
13
+ * This function should be used only on kotlin properties that have custom getters.
14
+ * In Release builds, such properties are compiled away into getter methods.
15
+ * In Debug builds, such properties exist as fields.
16
+ */
17
+ fun <T> getPropertyValueWithCustomGetter(instance: Any, propertyName: String): T? {
18
+ // In Release builds, properties are compiled away into getter methods.
19
+ val method = instance::class.memberFunctions.find { it.name == propertyName }
20
+ if (method != null) {
21
+ method.isAccessible = true
22
+ return method.call(instance) as T?
23
+ }
24
+
25
+ // In debug builds, properties exist as fields.
26
+ val property = instance::class.memberProperties.first { it.name == propertyName }
27
+ property.isAccessible = true
28
+ return (property as? kotlin.reflect.KProperty1<Any, *>)?.get(instance) as T?
29
+ }
30
+
31
+ }
@@ -8,7 +8,6 @@ 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
12
11
  import kotlinx.coroutines.Dispatchers
13
12
  import kotlinx.coroutines.runBlocking
14
13
  import kotlinx.coroutines.suspendCancellableCoroutine
@@ -171,20 +170,6 @@ object ViewHierarchyGenerator {
171
170
  attributes["text"] = view.text.toString()
172
171
  }
173
172
 
174
- // Inject animation metadata
175
- val animationMetadata = ViewLifecycleRegistry.getAnimationMetadata(view)
176
- if (animationMetadata != null) {
177
- animationMetadata.animated?.let {
178
- attributes["lastAnimated"] = animationMetadata.getAnimationDurationMs()?.toString() ?: "0"
179
- }
180
- animationMetadata.updated?.let {
181
- attributes["lastUpdated"] = animationMetadata.getUpdateDurationMs()?.toString() ?: "0"
182
- }
183
- if (ViewLifecycleRegistry.isAnimating(view)) {
184
- attributes["animating"] = "true"
185
- }
186
- }
187
-
188
173
  val currentTestId = view.tag?.toString() ?: ""
189
174
 
190
175
  val injectedPrefix = "detox_temp_"
@@ -6,15 +6,12 @@ import androidx.annotation.UiThread
6
6
  import androidx.test.espresso.IdlingResource.ResourceCallback
7
7
  import com.facebook.react.animated.NativeAnimatedModule
8
8
  import com.facebook.react.animated.NativeAnimatedNodesManager
9
- import com.facebook.react.bridge.ReactApplicationContext
10
9
  import com.facebook.react.bridge.ReactContext
11
10
  import com.wix.detox.common.DetoxErrors
12
11
  import com.wix.detox.common.DetoxLog.Companion.LOG_TAG
13
- import com.wix.detox.inquiry.FabricAnimationsInquirer
12
+ import com.wix.detox.common.KotlinReflectUtils
14
13
  import com.wix.detox.reactnative.ReactNativeInfo
15
14
  import com.wix.detox.reactnative.idlingresources.DetoxIdlingResource
16
- import kotlin.reflect.KProperty1
17
- import kotlin.reflect.full.memberFunctions
18
15
  import kotlin.reflect.full.memberProperties
19
16
  import kotlin.reflect.jvm.isAccessible
20
17
 
@@ -36,9 +33,6 @@ class AnimatedModuleIdlingResource(private val reactContext: ReactContext) : Det
36
33
 
37
34
  if (animatedModule.hasQueuedAnimations() ||
38
35
  animatedModule.hasActiveAnimations()) {
39
- if (reactContext is ReactApplicationContext) {
40
- FabricAnimationsInquirer.logAnimatingViews(reactContext)
41
- }
42
36
  Choreographer.getInstance().postFrameCallback(this)
43
37
  return false
44
38
  }
@@ -114,19 +108,8 @@ private class AnimatedModuleFacade(private val animatedModule: NativeAnimatedMod
114
108
 
115
109
  class OperationsQueueReflected(private val operationsQueue: Any) {
116
110
  fun isEmpty(): Boolean {
117
- // Try method first (works in release builds)
118
- val isEmptyMethod = operationsQueue::class.memberFunctions.find { it.name == "isEmpty" }
119
- if (isEmptyMethod != null) {
120
- isEmptyMethod.isAccessible = true
121
- return isEmptyMethod.call(operationsQueue) as Boolean
122
- }
123
-
124
- // Fallback to property (works in debug builds for RN 0.80+)
125
- val isEmptyProperty = operationsQueue::class.memberProperties.find { it.name == "isEmpty" }
126
- if (isEmptyProperty != null) {
127
- isEmptyProperty.isAccessible = true
128
- @Suppress("UNCHECKED_CAST")
129
- return (isEmptyProperty as KProperty1<Any, *>).get(operationsQueue) as Boolean
111
+ KotlinReflectUtils.getPropertyValueWithCustomGetter<Boolean>(operationsQueue, "isEmpty")?.let {
112
+ return it
130
113
  }
131
114
 
132
115
  throw DetoxErrors.DetoxIllegalStateException("isEmpty method/property cannot be reached")
@@ -25,6 +25,13 @@ object JavaTimersReflected {
25
25
  } else {
26
26
  "mReactHost"
27
27
  }
28
+
29
+ val reactInstanceFieldName = if (ReactNativeInfo.rnVersion().minor > 80) {
30
+ "reactInstance"
31
+ } else {
32
+ "mReactInstance"
33
+ }
34
+
28
35
  val javaTimerManagerFieldName = if (ReactNativeInfo.rnVersion().minor > 79) {
29
36
  "javaTimerManager"
30
37
  } else {
@@ -32,7 +39,7 @@ object JavaTimersReflected {
32
39
  }
33
40
 
34
41
  val reactHost = Reflect.on(reactContext).field(reactHostFieldName).get<Any>()
35
- val reactInstance = Reflect.on(reactHost).field("mReactInstance").get<Any>()
42
+ val reactInstance = Reflect.on(reactHost).field(reactInstanceFieldName).get<Any>()
36
43
  return Reflect.on(reactInstance).field(javaTimerManagerFieldName).get() as JavaTimerManager
37
44
  }
38
45
  }
@@ -5,6 +5,7 @@ import androidx.test.espresso.IdlingResource
5
5
  import com.facebook.react.bridge.ReactContext
6
6
  import com.facebook.react.uimanager.UIManagerHelper
7
7
  import com.facebook.react.uimanager.common.UIManagerType
8
+ import com.wix.detox.reactnative.ReactNativeInfo
8
9
  import com.wix.detox.reactnative.idlingresources.DetoxIdlingResource
9
10
  import org.joor.Reflect
10
11
  import java.util.concurrent.ConcurrentLinkedQueue
@@ -12,7 +13,7 @@ import java.util.concurrent.ConcurrentLinkedQueue
12
13
 
13
14
  class FabricUIManagerIdlingResources(
14
15
  private val reactContext: ReactContext
15
- ) : DetoxIdlingResource(), Choreographer.FrameCallback {
16
+ ) : DetoxIdlingResource(), Choreographer.FrameCallback {
16
17
 
17
18
  override fun checkIdle(): Boolean {
18
19
  return if (getViewCommandMountItemsSize() == 0 && getMountItemsSize() == 0) {
@@ -50,7 +51,12 @@ class FabricUIManagerIdlingResources(
50
51
 
51
52
  private fun getMountItemsSize(): Int {
52
53
  val mountItemDispatcher = getMountItemDispatcher()
53
- val mountItems = Reflect.on(mountItemDispatcher).field("mMountItems").get<ConcurrentLinkedQueue<*>>()
54
+ val filedName = if (ReactNativeInfo.rnVersion().minor >= 81) {
55
+ "mountItems"
56
+ } else {
57
+ "mMountItems"
58
+ }
59
+ val mountItems = Reflect.on(mountItemDispatcher).field(filedName).get<ConcurrentLinkedQueue<*>>()
54
60
  return mountItems.size
55
61
  }
56
62
 
@@ -62,8 +68,13 @@ class FabricUIManagerIdlingResources(
62
68
 
63
69
  private fun getViewCommandMountItemsSize(): Int {
64
70
  val mountItemDispatcher = getMountItemDispatcher()
71
+ val filedName = if (ReactNativeInfo.rnVersion().minor >= 81) {
72
+ "viewCommandMountItems"
73
+ } else {
74
+ "mViewCommandMountItems"
75
+ }
65
76
  val viewCommandMountItems =
66
- Reflect.on(mountItemDispatcher).field("mViewCommandMountItems").get<ConcurrentLinkedQueue<*>>()
77
+ Reflect.on(mountItemDispatcher).field(filedName).get<ConcurrentLinkedQueue<*>>()
67
78
  return viewCommandMountItems.size
68
79
  }
69
80
 
@@ -18,7 +18,7 @@ class UIModuleIdlingResource(private val reactContext: ReactContext)
18
18
  private val uiManagerModuleReflected = UIManagerModuleReflected(reactContext)
19
19
 
20
20
  override fun getName(): String = UIModuleIdlingResource::class.java.name
21
- override fun getDebugName(): String = " ui"
21
+ override fun getDebugName(): String = "ui"
22
22
  override fun getBusyHint(): Map<String, Any> {
23
23
  return mapOf("reason" to "UI rendering activity")
24
24
  }
@@ -3,17 +3,21 @@ package com.wix.detox.reactnative.ui
3
3
  import android.view.View
4
4
  import android.widget.TextView
5
5
  import com.wix.detox.common.traverseViewHierarchy
6
+ import com.wix.detox.reactnative.ReactNativeInfo
6
7
  import com.wix.detox.reactnative.utils.isReactNativeObject
7
8
 
8
9
  fun View.getAccessibilityLabel(
9
10
  isReactNativeObjectFn: (Any) -> Boolean = { isReactNativeObject(it) }
10
- ): CharSequence? =
11
- if (isReactNativeObjectFn(this)) {
11
+ ): CharSequence? {
12
+ val separator = if (ReactNativeInfo.rnVersion().minor >= 81) ", " else " "
13
+
14
+ return if (isReactNativeObjectFn(this)) {
12
15
  val subLabels = collectAccessibilityLabelsFromHierarchy(this)
13
- if (subLabels.isEmpty()) null else subLabels.joinToString(" ")
16
+ if (subLabels.isEmpty()) null else subLabels.joinToString(separator)
14
17
  } else {
15
18
  getRawAccessibilityLabel(this)
16
19
  }
20
+ }
17
21
 
18
22
  private fun collectAccessibilityLabelsFromHierarchy(
19
23
  rootView: View,
@@ -4,6 +4,7 @@ import android.view.View
4
4
  import android.view.ViewGroup
5
5
  import android.widget.TextView
6
6
  import com.wix.detox.UTHelpers.mockViewHierarchy
7
+ import com.wix.detox.reactnative.ReactNativeInfo
7
8
  import com.wix.detox.reactnative.ui.getAccessibilityLabel
8
9
  import org.assertj.core.api.Assertions
9
10
  import org.junit.Test
@@ -36,7 +37,8 @@ class UIExtensionsTest {
36
37
  fun `should return accessibility label according to children's content-description, recursively`() {
37
38
  val contentDescription1st = "cd.1"
38
39
  val contentDescription2nd = "cd.2"
39
- val expectedLabel = "$contentDescription1st $contentDescription2nd"
40
+ val expectedLabel =
41
+ if (ReactNativeInfo.rnVersion().minor >= 81) "$contentDescription1st, $contentDescription2nd" else "$contentDescription1st $contentDescription2nd"
40
42
 
41
43
 
42
44
  val parent: ViewGroup = mock()
@@ -1,6 +1,6 @@
1
1
  distributionBase=GRADLE_USER_HOME
2
2
  distributionPath=wrapper/dists
3
- distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
3
+ distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
4
4
  networkTimeout=10000
5
5
  validateDistributionUrl=true
6
6
  zipStoreBase=GRADLE_USER_HOME
@@ -47,4 +47,5 @@ ext.rnInfo = [
47
47
  isRN78OrHigher: rnMajorVer >= 78,
48
48
  isRN79OrHigher: rnMajorVer >= 79,
49
49
  isRN80OrHigher: rnMajorVer >= 80,
50
+ isRN81OrHigher: rnMajorVer >= 81,
50
51
  ]
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.44.0-smoke.4",
4
+ "version": "20.44.0",
5
5
  "bin": {
6
6
  "detox": "local-cli/cli.js"
7
7
  },
@@ -34,13 +34,13 @@
34
34
  "postinstall": "node scripts/postinstall.js"
35
35
  },
36
36
  "devDependencies": {
37
- "@react-native-community/cli": "19.1.1",
38
- "@react-native-community/cli-platform-android": "19.1.1",
39
- "@react-native-community/cli-platform-ios": "19.1.1",
40
- "@react-native/babel-preset": "0.80.2",
41
- "@react-native/eslint-config": "0.80.2",
42
- "@react-native/metro-config": "0.80.2",
43
- "@react-native/typescript-config": "0.80.2",
37
+ "@react-native-community/cli": "20.0.0",
38
+ "@react-native-community/cli-platform-android": "20.0.0",
39
+ "@react-native-community/cli-platform-ios": "20.0.0",
40
+ "@react-native/babel-preset": "0.82.0",
41
+ "@react-native/eslint-config": "0.82.0",
42
+ "@react-native/metro-config": "0.82.0",
43
+ "@react-native/typescript-config": "0.82.0",
44
44
  "@tsconfig/react-native": "^3.0.0",
45
45
  "@types/bunyan": "^1.8.8",
46
46
  "@types/child-process-promise": "^2.2.1",
@@ -62,9 +62,9 @@
62
62
  "jest-allure2-reporter": "^2.2.6",
63
63
  "metro-react-native-babel-preset": "0.76.8",
64
64
  "prettier": "^3.1.1",
65
- "react-native": "0.80.2",
65
+ "react-native": "0.82.0",
66
66
  "react-native-codegen": "^0.0.8",
67
- "typescript": "~5.3.3",
67
+ "typescript": "^5.8.3",
68
68
  "wtfnode": "^0.9.1"
69
69
  },
70
70
  "dependencies": {
@@ -120,5 +120,5 @@
120
120
  "browserslist": [
121
121
  "node 14"
122
122
  ],
123
- "gitHead": "35e4dca0f8f18ac2e348331d1ae7519f37e237a4"
123
+ "gitHead": "fb5940dbdf93e56185fed10f4adefecff37fcc77"
124
124
  }
@@ -6,7 +6,7 @@ const rnMinor = require('../src/utils/rn-consts/rn-consts').rnVersion.minor;
6
6
  function getGradleVersionByRNVersion() {
7
7
  switch (rnMinor) {
8
8
  default:
9
- return '8.14.1';
9
+ return '8.14.3';
10
10
  case '79':
11
11
  case '78':
12
12
  case '77':
@@ -1 +0,0 @@
1
- 1c1260d5d5dd7ba851908271b86c49c8
@@ -1 +0,0 @@
1
- f5fd22fc1bec6c426f5dbcfa7d57bcb3a2663583
@@ -1 +0,0 @@
1
- d0dcf3a7c33b5aeb99f910b1e264e3979c679531884ac40e3b8c00864890c253
@@ -1 +0,0 @@
1
- f096a65c66dc7241116db2c1dbd5cf1c86553e4a919b4e7ac9c61d618d25a0a5dedd7170fcd55dbf041a252856a32e124c5958eaa52d8aadaedc7c8adf6d0f44
@@ -1 +0,0 @@
1
- 21a721aa6c15fde7188e5fbad97cc6af
@@ -1 +0,0 @@
1
- b4d1dc9cfdd36505fabaccdf7a0c97c624a1d3ac
@@ -1 +0,0 @@
1
- 4abfe61356f07d39e8fc5291cd4aac0c34bceb987afdb8fef942f43cb07f33c4
@@ -1 +0,0 @@
1
- 442065e378030ae4f6cabbb777c80e335ad8c0902c5ed01869c8a18afa14b7fe4b523fd887f19027357dbd8eae8cefd1e18cbb1678ba0d680ef30a5944b8e619
@@ -1 +0,0 @@
1
- 852edb86e316ab26eafb9490e8069101
@@ -1 +0,0 @@
1
- fa2ef4968b4fd325cb5daab3504e95cf23be0004
@@ -1 +0,0 @@
1
- d998c8d633fc50c9df2155745628c50e3d30585e9f2aaeba00fb2b08cb996a35
@@ -1 +0,0 @@
1
- 91d26d9c5cc270127b659984508f52d84cca4e9c97ad54736d0a4bf6d9a661fd0df9797db449133dad5703f99ed62d28e8a8376b92629c309c8d0d1c37294015
@@ -1,629 +0,0 @@
1
- package com.wix.detox.inquiry
2
-
3
- import android.util.Log
4
- import android.util.SparseArray
5
- import android.view.View
6
- import com.facebook.react.animated.AnimatedNode
7
- import com.facebook.react.animated.NativeAnimatedModule
8
- import com.facebook.react.animated.NativeAnimatedNodesManager
9
- import com.facebook.react.bridge.ReactApplicationContext
10
- import com.facebook.react.uimanager.UIManagerHelper
11
- import com.facebook.react.uimanager.common.UIManagerType
12
- import java.lang.reflect.Field
13
- import java.util.LinkedList
14
- import java.util.concurrent.atomic.AtomicReference
15
-
16
- object FabricAnimationsInquirer {
17
- private const val LOG_TAG = "FabricAnimationsInquirer"
18
-
19
- // Cache fields to avoid repeated reflection lookups
20
- private var mActiveAnimationsField: Field? = null
21
- private var mUpdatedNodesField: Field? = null
22
- private var mAnimatedNodesField: Field? = null
23
- private var animatedValueField: Field? = null
24
- private var childrenField: Field? = null
25
- private var connectedViewTagField: Field? = null
26
- private var propNodeMappingField: Field? = null
27
- private var propMappingField: Field? = null
28
- private var nodeValueField: Field? = null
29
- private var offsetField: Field? = null
30
- private var preOperationsField: Field? = null
31
- private var operationsField: Field? = null
32
- private var mQueueField: Field? = null
33
- private var mPeekedOperationField: Field? = null
34
-
35
- fun logAnimatingViews(reactContext: ReactApplicationContext) {
36
- try {
37
- Log.d(LOG_TAG, "Starting animation inquiry...")
38
-
39
- // Clear previous animated views - fresh start for this inquiry
40
- ViewLifecycleRegistry.clearAnimatedViews()
41
-
42
- val nodesManager = getNodesManager(reactContext) ?: return
43
- Log.d(LOG_TAG, "Got nodesManager: ${nodesManager.javaClass.simpleName}")
44
-
45
- // Check for both queued and active animations
46
- val animatedModule = reactContext.getNativeModule(NativeAnimatedModule::class.java)
47
- if (animatedModule == null) {
48
- Log.d(LOG_TAG, "NativeAnimatedModule not found")
49
- return
50
- }
51
-
52
- // Check queued animations first
53
- val hasQueued = checkQueuedAnimations(animatedModule)
54
- Log.d(LOG_TAG, "hasQueuedAnimations() returned: $hasQueued")
55
-
56
- // Check active animations
57
- val hasActive = nodesManager.hasActiveAnimations()
58
- Log.d(LOG_TAG, "hasActiveAnimations() returned: $hasActive")
59
-
60
- if (!hasQueued && !hasActive) {
61
- Log.d(LOG_TAG, "No queued or active animations detected")
62
- return
63
- }
64
-
65
- if (hasQueued) {
66
- Log.d(LOG_TAG, "Found queued animations - analyzing operations queue")
67
- val queuedViewTags = analyzeQueuedOperations(animatedModule)
68
- if (queuedViewTags.isNotEmpty()) {
69
- Log.d(LOG_TAG, "Found ${queuedViewTags.size} views with queued animations: $queuedViewTags")
70
- logViews(reactContext, queuedViewTags)
71
- }
72
- }
73
-
74
- // Get all animated nodes from the graph
75
- val allNodes = getAllAnimatedNodes(nodesManager)
76
- Log.d(LOG_TAG, "Found ${allNodes.size()} total animated nodes")
77
-
78
- // Log the field names we're using for debugging
79
- Log.d(LOG_TAG, "Using field names: mActiveAnimations, mUpdatedNodes, mAnimatedNodes")
80
-
81
- // Find nodes that are currently animating (have active drivers or are updated)
82
- val animatingNodes = findAnimatingNodes(nodesManager, allNodes)
83
- Log.d(LOG_TAG, "Found ${animatingNodes.size} animating nodes")
84
-
85
- if (animatingNodes.isEmpty()) {
86
- Log.d(LOG_TAG, "No animating nodes found, exiting")
87
- return
88
- }
89
-
90
- // Find all relevant animated nodes (PropsAnimatedNode, StyleAnimatedNode, ValueAnimatedNode)
91
- val relevantNodes = findPropsNodes(animatingNodes, allNodes)
92
- Log.d(LOG_TAG, "Found ${relevantNodes.size} relevant animated nodes")
93
-
94
- if (relevantNodes.isEmpty()) {
95
- Log.d(LOG_TAG, "No relevant animated nodes found, exiting")
96
- return
97
- }
98
-
99
- val viewTags = getViewTags(relevantNodes, allNodes)
100
- Log.d(LOG_TAG, "Found ${viewTags.size} view tags: $viewTags")
101
-
102
- if (viewTags.isEmpty()) {
103
- Log.d(LOG_TAG, "No view tags found, exiting")
104
- return
105
- }
106
-
107
- logViews(reactContext, viewTags)
108
- } catch (e: Exception) {
109
- Log.e(LOG_TAG, "Failed to inquire animating views", e)
110
- }
111
- }
112
-
113
- private fun getNodesManager(reactContext: ReactApplicationContext): NativeAnimatedNodesManager? {
114
- val nativeAnimatedModule = reactContext.getNativeModule(NativeAnimatedModule::class.java)
115
- if (nativeAnimatedModule == null) {
116
- Log.d(LOG_TAG, "NativeAnimatedModule not found")
117
- return null
118
- }
119
-
120
- return try {
121
- // Use the public getNodesManager() method instead of reflection
122
- nativeAnimatedModule.nodesManager
123
- } catch (e: Exception) {
124
- Log.e(LOG_TAG, "Failed to get NativeAnimatedNodesManager via getNodesManager()", e)
125
- null
126
- }
127
- }
128
-
129
- private fun getAllAnimatedNodes(nodesManager: NativeAnimatedNodesManager): SparseArray<AnimatedNode> {
130
- val allNodes = SparseArray<AnimatedNode>()
131
- try {
132
- // Access mAnimatedNodes field using reflection
133
- val animatedNodesField = findOrCacheField(nodesManager.javaClass, "mAnimatedNodes", "mAnimatedNodesField")
134
- @Suppress("UNCHECKED_CAST")
135
- val animatedNodes = animatedNodesField?.get(nodesManager) as? SparseArray<AnimatedNode>
136
- if (animatedNodes != null) {
137
- Log.d(LOG_TAG, "Found ${animatedNodes.size()} animated nodes in graph")
138
- for (i in 0 until animatedNodes.size()) {
139
- val node = animatedNodes.valueAt(i)
140
- allNodes.put(animatedNodes.keyAt(i), node)
141
- }
142
- } else {
143
- Log.w(LOG_TAG, "Could not access mAnimatedNodes field")
144
- }
145
- } catch (e: Exception) {
146
- Log.e(LOG_TAG, "Failed to get all animated nodes", e)
147
- }
148
- return allNodes
149
- }
150
-
151
- private fun findAnimatingNodes(nodesManager: NativeAnimatedNodesManager, allNodes: SparseArray<AnimatedNode>): Set<AnimatedNode> {
152
- val animatingNodes = mutableSetOf<AnimatedNode>()
153
-
154
- try {
155
- // Get nodes from active animations
156
- val activeAnimationsField = findOrCacheField(nodesManager.javaClass, "mActiveAnimations", "mActiveAnimationsField")
157
- @Suppress("UNCHECKED_CAST")
158
- val activeAnimations = activeAnimationsField?.get(nodesManager) as? SparseArray<Any>
159
- if (activeAnimations != null) {
160
- Log.d(LOG_TAG, "Found ${activeAnimations.size()} active animations")
161
- for (i in 0 until activeAnimations.size()) {
162
- val driver = activeAnimations.valueAt(i)
163
- Log.d(LOG_TAG, "Active animation driver: ${driver.javaClass.simpleName}")
164
- val animatedValueField = findOrCacheField(driver.javaClass, "animatedValue", "animatedValueField")
165
- val valueNode = animatedValueField?.get(driver) as? AnimatedNode
166
- if (valueNode != null) {
167
- animatingNodes.add(valueNode)
168
- Log.d(LOG_TAG, "Added animating node from active animation: ${valueNode.javaClass.simpleName}")
169
- }
170
- }
171
- }
172
-
173
- // Get nodes from updated nodes
174
- val updatedNodesField = findOrCacheField(nodesManager.javaClass, "mUpdatedNodes", "mUpdatedNodesField")
175
- @Suppress("UNCHECKED_CAST")
176
- val updatedNodes = updatedNodesField?.get(nodesManager) as? SparseArray<AnimatedNode>
177
- if (updatedNodes != null) {
178
- Log.d(LOG_TAG, "Found ${updatedNodes.size()} updated nodes")
179
- for (i in 0 until updatedNodes.size()) {
180
- val node = updatedNodes.valueAt(i)
181
- animatingNodes.add(node)
182
- Log.d(LOG_TAG, "Added updated node: ${node.javaClass.simpleName}")
183
- }
184
- }
185
- } catch (e: Exception) {
186
- Log.e(LOG_TAG, "Failed to find animating nodes", e)
187
- }
188
-
189
- return animatingNodes
190
- }
191
-
192
- private fun findPropsNodes(animatingNodes: Set<AnimatedNode>, allNodes: SparseArray<AnimatedNode>): Set<Any> {
193
- val allRelevantNodes = mutableSetOf<Any>()
194
- val queue = LinkedList<AnimatedNode>(animatingNodes)
195
- val visited = mutableSetOf<AnimatedNode>()
196
-
197
- while (queue.isNotEmpty()) {
198
- val node = queue.poll() ?: continue
199
- if (node in visited) {
200
- continue
201
- }
202
- visited.add(node)
203
-
204
- // Check what type of node this is and log accordingly
205
- val nodeType = node.javaClass.simpleName
206
- when (nodeType) {
207
- "PropsAnimatedNode" -> {
208
- allRelevantNodes.add(node as Any)
209
- Log.d(LOG_TAG, "Found PropsAnimatedNode: $nodeType")
210
- }
211
- "StyleAnimatedNode" -> {
212
- allRelevantNodes.add(node as Any)
213
- Log.d(LOG_TAG, "Found StyleAnimatedNode: $nodeType")
214
- }
215
- "ValueAnimatedNode" -> {
216
- allRelevantNodes.add(node as Any)
217
- Log.d(LOG_TAG, "Found ValueAnimatedNode: $nodeType")
218
- }
219
- else -> {
220
- Log.d(LOG_TAG, "Found other animated node: $nodeType")
221
- }
222
- }
223
-
224
- // Traverse children to find more nodes
225
- try {
226
- val childrenField = findOrCacheField(node.javaClass, "children", "childrenField")
227
- @Suppress("UNCHECKED_CAST")
228
- val children = childrenField?.get(node) as? List<AnimatedNode>
229
- if (children != null) {
230
- queue.addAll(children)
231
- }
232
- } catch (e: Exception) {
233
- // Ignored - not all nodes have children
234
- }
235
- }
236
-
237
- return allRelevantNodes
238
- }
239
-
240
- private fun isPropsAnimatedNode(node: AnimatedNode): Boolean {
241
- return try {
242
- // Check if this is actually a PropsAnimatedNode by checking the class name
243
- // and verifying it has the connectedViewTag field
244
- val isPropsNode = node.javaClass.simpleName == "PropsAnimatedNode"
245
- if (isPropsNode) {
246
- val connectedViewTagField = findOrCacheField(node.javaClass, "connectedViewTag", "connectedViewTagField")
247
- connectedViewTagField != null
248
- } else {
249
- false
250
- }
251
- } catch (e: Exception) {
252
- false
253
- }
254
- }
255
-
256
- private fun getViewTags(relevantNodes: Set<Any>, allNodes: SparseArray<AnimatedNode>): Set<Int> {
257
- val viewTags = mutableSetOf<Int>()
258
- for (node in relevantNodes) {
259
- try {
260
- val nodeType = node.javaClass.simpleName
261
- Log.d(LOG_TAG, "Processing $nodeType for view tags")
262
-
263
- when (nodeType) {
264
- "PropsAnimatedNode" -> {
265
- val connectedViewTagField = findOrCacheField(node.javaClass, "connectedViewTag", "connectedViewTagField")
266
- val viewTag = connectedViewTagField?.get(node) as? Int
267
- if (viewTag != null && viewTag != -1) {
268
- viewTags.add(viewTag)
269
- Log.d(LOG_TAG, "PropsAnimatedNode connected to view tag: $viewTag")
270
-
271
- // Log the property mapping to see what properties are being animated
272
- try {
273
- val propNodeMappingField = findOrCacheField(node.javaClass, "propNodeMapping", "propNodeMappingField")
274
- @Suppress("UNCHECKED_CAST")
275
- val propNodeMapping = propNodeMappingField?.get(node) as? Map<String, Int>
276
- if (propNodeMapping != null) {
277
- Log.d(LOG_TAG, "View $viewTag has animated properties: ${propNodeMapping.keys}")
278
- }
279
- } catch (e: Exception) {
280
- Log.d(LOG_TAG, "Could not access propNodeMapping for PropsAnimatedNode")
281
- }
282
- }
283
- }
284
- "StyleAnimatedNode" -> {
285
- // StyleAnimatedNode doesn't have connectedViewTag field - it's connected through PropsAnimatedNode
286
- Log.d(LOG_TAG, "StyleAnimatedNode has no direct view connection (connected through PropsAnimatedNode)")
287
-
288
- // Try to access propMapping to see what properties it handles
289
- try {
290
- val propMappingField = findOrCacheField(node.javaClass, "propMapping", "propMappingField")
291
- @Suppress("UNCHECKED_CAST")
292
- val propMapping = propMappingField?.get(node) as? Map<String, Int>
293
- if (propMapping != null) {
294
- Log.d(LOG_TAG, "StyleAnimatedNode handles properties: ${propMapping.keys}")
295
- }
296
- } catch (e: Exception) {
297
- Log.d(LOG_TAG, "Could not access StyleAnimatedNode propMapping: ${e.message}")
298
- }
299
-
300
- // Find the connected view by traversing the graph to find PropsAnimatedNode
301
- val connectedViewTag = findConnectedViewThroughGraph(node as AnimatedNode, allNodes)
302
- if (connectedViewTag != -1) {
303
- viewTags.add(connectedViewTag)
304
- Log.d(LOG_TAG, "StyleAnimatedNode connected to view tag through graph: $connectedViewTag")
305
- } else {
306
- Log.w(LOG_TAG, "StyleAnimatedNode has no connected view - this could mean:")
307
- Log.w(LOG_TAG, " 1. No PropsAnimatedNode found in graph traversal")
308
- Log.w(LOG_TAG, " 2. PropsAnimatedNode exists but not connected to any view")
309
- Log.w(LOG_TAG, " 3. Graph traversal failed due to reflection errors")
310
- Log.w(LOG_TAG, " 4. Circular references or disconnected graph")
311
- }
312
- }
313
- "ValueAnimatedNode" -> {
314
- // ValueAnimatedNode typically doesn't have direct view connection
315
- // but we can log its value for debugging
316
- try {
317
- val nodeValueField = findOrCacheField(node.javaClass, "nodeValue", "nodeValueField")
318
- val nodeValue = nodeValueField?.get(node) as? Double
319
- val offsetField = findOrCacheField(node.javaClass, "offset", "offsetField")
320
- val offset = offsetField?.get(node) as? Double
321
- Log.d(LOG_TAG, "ValueAnimatedNode value: $nodeValue, offset: $offset")
322
- } catch (e: Exception) {
323
- Log.d(LOG_TAG, "Could not access ValueAnimatedNode values: ${e.message}")
324
- }
325
- }
326
- else -> {
327
- Log.d(LOG_TAG, "Unknown node type: $nodeType")
328
- }
329
- }
330
- } catch (e: Exception) {
331
- Log.e(LOG_TAG, "Failed to process node: ${node.javaClass.simpleName}", e)
332
- }
333
- }
334
- return viewTags
335
- }
336
-
337
- private fun logViews(reactContext: ReactApplicationContext, viewTags: Set<Int>) {
338
- val uiManager = UIManagerHelper.getUIManager(reactContext, UIManagerType.FABRIC)
339
- if (uiManager == null) {
340
- Log.w(LOG_TAG, "Fabric UIManager not found.")
341
- return
342
- }
343
-
344
- for (tag in viewTags) {
345
- try {
346
- reactContext.runOnUiQueueThread {
347
- val view = uiManager.resolveView(tag)
348
- if (view != null) {
349
- ViewLifecycleRegistry.markAnimated(view)
350
-
351
- // Get view coordinates and dimensions
352
- val left = view.left
353
- val top = view.top
354
- val right = view.right
355
- val bottom = view.bottom
356
- val width = right - left
357
- val height = bottom - top
358
-
359
- Log.i(LOG_TAG, "Animating view: tag=$tag, class=${view.javaClass.simpleName}, id=${view.id}, " +
360
- "bounds=[$left,$top,$right,$bottom], size=${width}x${height}")
361
- } else {
362
- Log.w(LOG_TAG, "Could not resolve view for tag: $tag")
363
- }
364
- }
365
- } catch (e: Exception) {
366
- Log.e(LOG_TAG, "Failed to resolve or log view for tag: $tag", e)
367
- }
368
- }
369
- }
370
-
371
- private fun findOrCacheField(clazz: Class<*>, fieldName: String, cacheFieldName: String): Field? {
372
- try {
373
- val cacheField = FabricAnimationsInquirer::class.java.getDeclaredField(cacheFieldName).apply { isAccessible = true }
374
- var field = cacheField.get(this) as? Field
375
- if (field == null) {
376
- field = findFieldRecursive(clazz, fieldName)
377
- if (field != null) {
378
- cacheField.set(this, field)
379
- }
380
- }
381
- return field
382
- } catch (e: Exception) {
383
- Log.w(LOG_TAG, "Could not find or cache field $fieldName", e)
384
- return null
385
- }
386
- }
387
-
388
- private fun findFieldRecursive(clazz: Class<*>, fieldName: String): Field? {
389
- var currentClass: Class<*>? = clazz
390
- while (currentClass != null && currentClass != Any::class.java) {
391
- try {
392
- return currentClass.getDeclaredField(fieldName).apply { isAccessible = true }
393
- } catch (e: NoSuchFieldException) {
394
- // Not in this class, check superclass
395
- }
396
- currentClass = currentClass.superclass
397
- }
398
- Log.w(LOG_TAG, "Field '$fieldName' not found in class hierarchy for '${clazz.simpleName}'")
399
- return null
400
- }
401
-
402
- private fun findConnectedViewThroughGraph(startNode: AnimatedNode, allNodes: SparseArray<AnimatedNode>): Int {
403
- val queue = LinkedList<AnimatedNode>()
404
- val visited = mutableSetOf<AnimatedNode>()
405
- var propsNodesFound = 0
406
- var propsNodesWithView = 0
407
- var propsNodesWithoutView = 0
408
-
409
- Log.d(LOG_TAG, "Starting graph traversal from ${startNode.javaClass.simpleName}")
410
-
411
- queue.add(startNode)
412
- visited.add(startNode)
413
-
414
- while (queue.isNotEmpty()) {
415
- val node = queue.poll()
416
- val nodeType = node.javaClass.simpleName
417
-
418
- // Check if this is a PropsAnimatedNode with a connected view
419
- if (nodeType == "PropsAnimatedNode") {
420
- propsNodesFound++
421
- try {
422
- val connectedViewTagField = findOrCacheField(node.javaClass, "connectedViewTag", "connectedViewTagField")
423
- val viewTag = connectedViewTagField?.get(node) as? Int
424
- if (viewTag != null && viewTag != -1) {
425
- propsNodesWithView++
426
- Log.d(LOG_TAG, "Found PropsAnimatedNode with connected view: $viewTag")
427
- return viewTag
428
- } else {
429
- propsNodesWithoutView++
430
- Log.d(LOG_TAG, "Found PropsAnimatedNode but no connected view (viewTag: $viewTag)")
431
- }
432
- } catch (e: Exception) {
433
- Log.w(LOG_TAG, "Failed to access connectedViewTag from PropsAnimatedNode: ${e.message}")
434
- }
435
- }
436
-
437
- // Traverse children to find PropsAnimatedNode
438
- try {
439
- val childrenField = findOrCacheField(node.javaClass, "children", "childrenField")
440
- @Suppress("UNCHECKED_CAST")
441
- val children = childrenField?.get(node) as? List<AnimatedNode>
442
- if (children != null) {
443
- Log.d(LOG_TAG, "Traversing ${children.size} children from $nodeType")
444
- for (child in children) {
445
- if (child !in visited) {
446
- visited.add(child)
447
- queue.add(child)
448
- }
449
- }
450
- } else {
451
- Log.d(LOG_TAG, "$nodeType has no children")
452
- }
453
- } catch (e: Exception) {
454
- Log.d(LOG_TAG, "Could not access children from $nodeType: ${e.message}")
455
- }
456
- }
457
-
458
- Log.w(LOG_TAG, "Graph traversal completed:")
459
- Log.w(LOG_TAG, " - Total nodes visited: ${visited.size}")
460
- Log.w(LOG_TAG, " - PropsAnimatedNodes found: $propsNodesFound")
461
- Log.w(LOG_TAG, " - PropsAnimatedNodes with view: $propsNodesWithView")
462
- Log.w(LOG_TAG, " - PropsAnimatedNodes without view: $propsNodesWithoutView")
463
-
464
- return -1
465
- }
466
-
467
- private fun checkQueuedAnimations(animatedModule: NativeAnimatedModule): Boolean {
468
- return try {
469
- // Check mPreOperations queue (Android field name)
470
- val preOperationsField = findOrCacheField(animatedModule.javaClass, "mPreOperations", "preOperationsField")
471
- val preOperations = preOperationsField?.get(animatedModule)
472
- val preOperationsEmpty = checkOperationsQueueEmpty(preOperations)
473
- Log.d(LOG_TAG, "mPreOperations queue empty: $preOperationsEmpty")
474
-
475
- // Check mOperations queue (Android field name)
476
- val operationsField = findOrCacheField(animatedModule.javaClass, "mOperations", "operationsField")
477
- val operations = operationsField?.get(animatedModule)
478
- val operationsEmpty = checkOperationsQueueEmpty(operations)
479
- Log.d(LOG_TAG, "mOperations queue empty: $operationsEmpty")
480
-
481
- val hasQueued = !preOperationsEmpty || !operationsEmpty
482
- Log.d(LOG_TAG, "Has queued animations: $hasQueued")
483
- hasQueued
484
- } catch (e: Exception) {
485
- Log.e(LOG_TAG, "Failed to check queued animations", e)
486
- false
487
- }
488
- }
489
-
490
- private fun checkOperationsQueueEmpty(operationsQueue: Any?): Boolean {
491
- if (operationsQueue == null) {
492
- Log.d(LOG_TAG, "Operations queue is null")
493
- return true
494
- }
495
-
496
- return try {
497
- // Try to get isEmpty method
498
- val isEmptyMethod = operationsQueue.javaClass.getDeclaredMethod("isEmpty")
499
- isEmptyMethod.isAccessible = true
500
- val isEmpty = isEmptyMethod.invoke(operationsQueue) as Boolean
501
- Log.d(LOG_TAG, "Operations queue isEmpty() result: $isEmpty")
502
- isEmpty
503
- } catch (e: Exception) {
504
- Log.d(LOG_TAG, "Could not call isEmpty() on operations queue, trying property access")
505
- try {
506
- // Fallback to property access
507
- val isEmptyProperty = operationsQueue.javaClass.getDeclaredField("isEmpty")
508
- isEmptyProperty.isAccessible = true
509
- val isEmpty = isEmptyProperty.get(operationsQueue) as Boolean
510
- Log.d(LOG_TAG, "Operations queue isEmpty property: $isEmpty")
511
- isEmpty
512
- } catch (e2: Exception) {
513
- Log.e(LOG_TAG, "Could not access isEmpty property on operations queue", e2)
514
- true // Assume empty if we can't determine
515
- }
516
- }
517
- }
518
-
519
- private fun analyzeQueuedOperations(animatedModule: NativeAnimatedModule): Set<Int> {
520
- val viewTags = mutableSetOf<Int>()
521
- try {
522
- Log.d(LOG_TAG, "Analyzing queued operations...")
523
-
524
- // Analyze mPreOperations queue (Android field name)
525
- val preOperationsField = findOrCacheField(animatedModule.javaClass, "mPreOperations", "preOperationsField")
526
- val preOperations = preOperationsField?.get(animatedModule)
527
- if (preOperations != null) {
528
- Log.d(LOG_TAG, "Analyzing mPreOperations queue...")
529
- val preOpsViewTags = analyzeOperationsQueue(preOperations, "mPreOperations")
530
- viewTags.addAll(preOpsViewTags)
531
- }
532
-
533
- // Analyze mOperations queue (Android field name)
534
- val operationsField = findOrCacheField(animatedModule.javaClass, "mOperations", "operationsField")
535
- val operations = operationsField?.get(animatedModule)
536
- if (operations != null) {
537
- Log.d(LOG_TAG, "Analyzing mOperations queue...")
538
- val opsViewTags = analyzeOperationsQueue(operations, "mOperations")
539
- viewTags.addAll(opsViewTags)
540
- }
541
- } catch (e: Exception) {
542
- Log.e(LOG_TAG, "Failed to analyze queued operations", e)
543
- }
544
- return viewTags
545
- }
546
-
547
- private fun analyzeOperationsQueue(operationsQueue: Any, queueName: String): Set<Int> {
548
- val viewTags = mutableSetOf<Int>()
549
- try {
550
- // Log queue class information first
551
- Log.d(LOG_TAG, "$queueName queue class: ${operationsQueue.javaClass.simpleName}")
552
- Log.d(LOG_TAG, "$queueName queue fields: ${operationsQueue.javaClass.declaredFields.map { it.name }}")
553
-
554
- // Try to access the internal mQueue field (ConcurrentLinkedQueue)
555
- val mQueueField = findOrCacheField(operationsQueue.javaClass, "mQueue", "mQueueField")
556
- if (mQueueField != null) {
557
- val mQueue = mQueueField.get(operationsQueue)
558
- Log.d(LOG_TAG, "Found mQueue in $queueName: $mQueue")
559
-
560
- // Try to get queue size
561
- try {
562
- val sizeMethod = mQueue.javaClass.getDeclaredMethod("size")
563
- sizeMethod.isAccessible = true
564
- val size = sizeMethod.invoke(mQueue) as Int
565
- Log.d(LOG_TAG, "$queueName mQueue size: $size")
566
- } catch (e: Exception) {
567
- Log.d(LOG_TAG, "Could not get size of $queueName mQueue")
568
- }
569
-
570
- // Try to peek at queue contents (without removing)
571
- try {
572
- val peekMethod = mQueue.javaClass.getDeclaredMethod("peek")
573
- peekMethod.isAccessible = true
574
- val peekedOperation = peekMethod.invoke(mQueue)
575
- if (peekedOperation != null) {
576
- Log.d(LOG_TAG, "Peeked operation from $queueName: ${peekedOperation.javaClass.simpleName}")
577
-
578
- // Try to extract view tags from the operation
579
- val operationViewTags = extractViewTagsFromOperation(peekedOperation)
580
- viewTags.addAll(operationViewTags)
581
- }
582
- } catch (e: Exception) {
583
- Log.d(LOG_TAG, "Could not peek at $queueName queue contents: ${e.message}")
584
- }
585
- }
586
-
587
- // Try to access mPeekedOperation field
588
- val mPeekedOperationField = findOrCacheField(operationsQueue.javaClass, "mPeekedOperation", "mPeekedOperationField")
589
- if (mPeekedOperationField != null) {
590
- val mPeekedOperation = mPeekedOperationField.get(operationsQueue)
591
- if (mPeekedOperation != null) {
592
- Log.d(LOG_TAG, "Found mPeekedOperation in $queueName: ${mPeekedOperation.javaClass.simpleName}")
593
-
594
- // Try to extract view tags from the peeked operation
595
- val peekedViewTags = extractViewTagsFromOperation(mPeekedOperation)
596
- viewTags.addAll(peekedViewTags)
597
- }
598
- }
599
-
600
- } catch (e: Exception) {
601
- Log.e(LOG_TAG, "Failed to analyze $queueName queue", e)
602
- }
603
- return viewTags
604
- }
605
-
606
- private fun extractViewTagsFromOperation(operation: Any): Set<Int> {
607
- val viewTags = mutableSetOf<Int>()
608
- try {
609
- Log.d(LOG_TAG, "Analyzing operation: ${operation.javaClass.simpleName}")
610
-
611
- // Try to find view tags in the operation's fields
612
- val fields = operation.javaClass.declaredFields
613
- for (field in fields) {
614
- field.isAccessible = true
615
- val value = field.get(operation)
616
- Log.d(LOG_TAG, "Operation field ${field.name}: $value (${value?.javaClass?.simpleName})")
617
-
618
- // Look for integer fields that might be view tags
619
- if (value is Int && value > 0) {
620
- Log.d(LOG_TAG, "Found potential view tag: $value")
621
- viewTags.add(value)
622
- }
623
- }
624
- } catch (e: Exception) {
625
- Log.d(LOG_TAG, "Failed to extract view tags from operation: ${e.message}")
626
- }
627
- return viewTags
628
- }
629
- }
@@ -1,143 +0,0 @@
1
- package com.wix.detox.inquiry
2
-
3
- import android.util.Log
4
- import android.view.View
5
- import java.util.concurrent.ConcurrentHashMap
6
- import java.util.Date
7
-
8
- /**
9
- * Registry to track view lifecycle events like mounting, updating, and animating.
10
- * This data is used to inject metadata into the XML hierarchy for debugging.
11
- */
12
- object ViewLifecycleRegistry {
13
- private const val LOG_TAG = "ViewLifecycleRegistry"
14
-
15
- // Thread-safe maps to store view lifecycle data
16
- private val mountedViews = ConcurrentHashMap<View, Date>()
17
- private val updatedViews = ConcurrentHashMap<View, Date>()
18
- private val animatedViews = ConcurrentHashMap<View, Date>()
19
- private val customEvents = ConcurrentHashMap<View, MutableList<Pair<String, Date>>>()
20
-
21
- /**
22
- * Mark a view as mounted (created/attached)
23
- */
24
- fun markMounted(view: View) {
25
- val now = Date()
26
- mountedViews[view] = now
27
- Log.d(LOG_TAG, "View mounted: ${view.javaClass.simpleName} at $now")
28
- }
29
-
30
- /**
31
- * Mark a view as updated (props changed)
32
- */
33
- fun markUpdated(view: View) {
34
- val now = Date()
35
- updatedViews[view] = now
36
- Log.d(LOG_TAG, "View updated: ${view.javaClass.simpleName} at $now")
37
- }
38
-
39
- /**
40
- * Clear animated views older than 5 seconds (called at start of each inquiry)
41
- */
42
- fun clearAnimatedViews() {
43
- val now = System.currentTimeMillis()
44
- val fiveSecondsAgo = now - 5000
45
-
46
- val iterator = animatedViews.iterator()
47
- var clearedCount = 0
48
-
49
- while (iterator.hasNext()) {
50
- val entry = iterator.next()
51
- if (entry.value.time < fiveSecondsAgo) {
52
- iterator.remove()
53
- clearedCount++
54
- }
55
- }
56
-
57
- Log.d(LOG_TAG, "Cleared $clearedCount animated views older than 5s, ${animatedViews.size} remaining")
58
- }
59
-
60
- /**
61
- * Mark a view as currently animating
62
- */
63
- fun markAnimated(view: View) {
64
- val now = Date()
65
- animatedViews[view] = now
66
- Log.d(LOG_TAG, "View animating: ${view.javaClass.simpleName} at $now")
67
- }
68
-
69
- /**
70
- * Mark a custom event on a view (e.g., specific animated properties)
71
- */
72
- fun markCustomEvent(view: View, event: String) {
73
- val now = Date()
74
- customEvents.computeIfAbsent(view) { mutableListOf() }.add(event to now)
75
- Log.d(LOG_TAG, "Custom event '$event' on view: ${view.javaClass.simpleName} at $now")
76
- }
77
-
78
- /**
79
- * Get animation metadata for a view
80
- */
81
- fun getAnimationMetadata(view: View): AnimationMetadata? {
82
- val mounted = mountedViews[view]
83
- val updated = updatedViews[view]
84
- val animated = animatedViews[view]
85
- val events = customEvents[view] ?: emptyList()
86
-
87
- if (mounted == null && updated == null && animated == null && events.isEmpty()) {
88
- return null
89
- }
90
-
91
- return AnimationMetadata(
92
- mounted = mounted,
93
- updated = updated,
94
- animated = animated,
95
- events = events
96
- )
97
- }
98
-
99
- /**
100
- * Clear all data (useful for testing)
101
- */
102
- fun clear() {
103
- mountedViews.clear()
104
- updatedViews.clear()
105
- animatedViews.clear()
106
- customEvents.clear()
107
- Log.d(LOG_TAG, "ViewLifecycleRegistry cleared")
108
- }
109
-
110
- /**
111
- * Get all currently animated views
112
- */
113
- fun getAnimatedViews(): Map<View, Date> = animatedViews.toMap()
114
-
115
- /**
116
- * Check if a view is currently animating
117
- */
118
- fun isAnimating(view: View): Boolean = animatedViews.containsKey(view)
119
- }
120
-
121
- /**
122
- * Data class to hold animation metadata for a view
123
- */
124
- data class AnimationMetadata(
125
- val mounted: Date?,
126
- val updated: Date?,
127
- val animated: Date?,
128
- val events: List<Pair<String, Date>>
129
- ) {
130
- /**
131
- * Calculate time since animation started in milliseconds
132
- */
133
- fun getAnimationDurationMs(): Long? {
134
- return animated?.let { System.currentTimeMillis() - it.time }
135
- }
136
-
137
- /**
138
- * Calculate time since last update in milliseconds
139
- */
140
- fun getUpdateDurationMs(): Long? {
141
- return updated?.let { System.currentTimeMillis() - it.time }
142
- }
143
- }