detox 20.44.0-smoke.3 → 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.3/detox-20.44.0-smoke.3-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.3/detox-20.44.0-smoke.3.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.3/detox-20.44.0-smoke.3-sources.jar.md5 +0 -1
  38. package/Detox-android/com/wix/detox/20.44.0-smoke.3/detox-20.44.0-smoke.3-sources.jar.sha1 +0 -1
  39. package/Detox-android/com/wix/detox/20.44.0-smoke.3/detox-20.44.0-smoke.3-sources.jar.sha256 +0 -1
  40. package/Detox-android/com/wix/detox/20.44.0-smoke.3/detox-20.44.0-smoke.3-sources.jar.sha512 +0 -1
  41. package/Detox-android/com/wix/detox/20.44.0-smoke.3/detox-20.44.0-smoke.3.aar +0 -0
  42. package/Detox-android/com/wix/detox/20.44.0-smoke.3/detox-20.44.0-smoke.3.aar.md5 +0 -1
  43. package/Detox-android/com/wix/detox/20.44.0-smoke.3/detox-20.44.0-smoke.3.aar.sha1 +0 -1
  44. package/Detox-android/com/wix/detox/20.44.0-smoke.3/detox-20.44.0-smoke.3.aar.sha256 +0 -1
  45. package/Detox-android/com/wix/detox/20.44.0-smoke.3/detox-20.44.0-smoke.3.aar.sha512 +0 -1
  46. package/Detox-android/com/wix/detox/20.44.0-smoke.3/detox-20.44.0-smoke.3.pom.md5 +0 -1
  47. package/Detox-android/com/wix/detox/20.44.0-smoke.3/detox-20.44.0-smoke.3.pom.sha1 +0 -1
  48. package/Detox-android/com/wix/detox/20.44.0-smoke.3/detox-20.44.0-smoke.3.pom.sha256 +0 -1
  49. package/Detox-android/com/wix/detox/20.44.0-smoke.3/detox-20.44.0-smoke.3.pom.sha512 +0 -1
  50. package/android/detox/src/full/java/com/wix/detox/inquiry/FabricAnimationsInquirer.kt +0 -440
  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.3</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.3</latest>
7
- <release>20.44.0-smoke.3</release>
6
+ <latest>20.44.0</latest>
7
+ <release>20.44.0</release>
8
8
  <versions>
9
- <version>20.44.0-smoke.3</version>
9
+ <version>20.44.0</version>
10
10
  </versions>
11
- <lastUpdated>20251009174711</lastUpdated>
11
+ <lastUpdated>20251019160654</lastUpdated>
12
12
  </versioning>
13
13
  </metadata>
@@ -1 +1 @@
1
- c8acd83b4c64653b9e9ba87a7e37abf1
1
+ 129e8845e2c49ebb6a8a08917ba2f5ca
@@ -1 +1 @@
1
- fa3e431bbd58b9c608fe0396caa81eb7b2123391
1
+ 5155667329c4ec88cdebcd3eec1e00ab5cc18aab
@@ -1 +1 @@
1
- 4c4fe8daadb8a5ecad499fd25cf6b5556dbfab190c98280df02898da11bf7b71
1
+ 3141cb84e424059e9ed45660a82052b581406ab15a4fc8c400df3c1950fae08c
@@ -1 +1 @@
1
- da8e523ed536ed9515efe564b4497e1a2872a5860aacd2388fe37ab6b12a1df3d40c3a30c5e58a33b03fec0a585d89807ea9e23cae35408024ade6c199b09daa
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.3",
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": "69e5e1f5fadd16c25e4303c7bda877f556527c5e"
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
- 6c1c629a4bb9fb113a38d3f2babcdaf9
@@ -1 +0,0 @@
1
- 016b3ca18886178b7a615db51c442a411d0dcc76
@@ -1 +0,0 @@
1
- 25c112c9be84680ae6ea75a3ad09ac53ad18d42f0e66ffd362047cf4cadeec13
@@ -1 +0,0 @@
1
- 2bae6abf332074d5a949d58ecd2499af1493b953d7ee8b2b322ab7b384c03608171d997dceac4d114973636703748f480a189a7ccaa27865665ec7c2e051b090
@@ -1 +0,0 @@
1
- c3f8d3dc2d7edb9ccf2e95efca9450ca
@@ -1 +0,0 @@
1
- e0bcddf47775a5112e413fea3790fa565092df86
@@ -1 +0,0 @@
1
- 2230846f74335e85d6edd07294869e1604efdda6b98fcf0abaff34b85b18f3fb
@@ -1 +0,0 @@
1
- bcff1f76753fe3725089dc1e4130aa2f4100907b1912122bf461ebe6bd588f7cf5b4c6cdb8a7eeaa84b957e511d206563d8a2cf7c0d0cef1357959448e86c483
@@ -1 +0,0 @@
1
- d1b54d72ec1e32909012654ebb143147
@@ -1 +0,0 @@
1
- bfaa5fed850535443ca38a8d7bb765f0629ffa37
@@ -1 +0,0 @@
1
- 4ad653fa6e356e01536665021591f24d8fddc7d9e37490b9136d3ed8ed48e09f
@@ -1 +0,0 @@
1
- 0e53116005919ab7a58d00f77e3649e31c1592b18837fa91b5ed998c5eea52d08f2067b3f54d15ba053d683f54158f5404321e00cf2e67ff64f99455ca1d05df
@@ -1,440 +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
-
31
- fun logAnimatingViews(reactContext: ReactApplicationContext) {
32
- try {
33
- Log.d(LOG_TAG, "Starting animation inquiry...")
34
-
35
- // Clear previous animated views - fresh start for this inquiry
36
- ViewLifecycleRegistry.clearAnimatedViews()
37
-
38
- val nodesManager = getNodesManager(reactContext) ?: return
39
- Log.d(LOG_TAG, "Got nodesManager: ${nodesManager.javaClass.simpleName}")
40
-
41
- // Check if there are any active animations first
42
- val hasActive = nodesManager.hasActiveAnimations()
43
- Log.d(LOG_TAG, "hasActiveAnimations() returned: $hasActive")
44
-
45
- if (!hasActive) {
46
- Log.d(LOG_TAG, "No active animations detected")
47
- return
48
- }
49
-
50
- // Get all animated nodes from the graph
51
- val allNodes = getAllAnimatedNodes(nodesManager)
52
- Log.d(LOG_TAG, "Found ${allNodes.size()} total animated nodes")
53
-
54
- // Log the field names we're using for debugging
55
- Log.d(LOG_TAG, "Using field names: mActiveAnimations, mUpdatedNodes, mAnimatedNodes")
56
-
57
- // Find nodes that are currently animating (have active drivers or are updated)
58
- val animatingNodes = findAnimatingNodes(nodesManager, allNodes)
59
- Log.d(LOG_TAG, "Found ${animatingNodes.size} animating nodes")
60
-
61
- if (animatingNodes.isEmpty()) {
62
- Log.d(LOG_TAG, "No animating nodes found, exiting")
63
- return
64
- }
65
-
66
- // Find all relevant animated nodes (PropsAnimatedNode, StyleAnimatedNode, ValueAnimatedNode)
67
- val relevantNodes = findPropsNodes(animatingNodes, allNodes)
68
- Log.d(LOG_TAG, "Found ${relevantNodes.size} relevant animated nodes")
69
-
70
- if (relevantNodes.isEmpty()) {
71
- Log.d(LOG_TAG, "No relevant animated nodes found, exiting")
72
- return
73
- }
74
-
75
- val viewTags = getViewTags(relevantNodes, allNodes)
76
- Log.d(LOG_TAG, "Found ${viewTags.size} view tags: $viewTags")
77
-
78
- if (viewTags.isEmpty()) {
79
- Log.d(LOG_TAG, "No view tags found, exiting")
80
- return
81
- }
82
-
83
- logViews(reactContext, viewTags)
84
- } catch (e: Exception) {
85
- Log.e(LOG_TAG, "Failed to inquire animating views", e)
86
- }
87
- }
88
-
89
- private fun getNodesManager(reactContext: ReactApplicationContext): NativeAnimatedNodesManager? {
90
- val nativeAnimatedModule = reactContext.getNativeModule(NativeAnimatedModule::class.java)
91
- if (nativeAnimatedModule == null) {
92
- Log.d(LOG_TAG, "NativeAnimatedModule not found")
93
- return null
94
- }
95
-
96
- return try {
97
- // Use the public getNodesManager() method instead of reflection
98
- nativeAnimatedModule.nodesManager
99
- } catch (e: Exception) {
100
- Log.e(LOG_TAG, "Failed to get NativeAnimatedNodesManager via getNodesManager()", e)
101
- null
102
- }
103
- }
104
-
105
- private fun getAllAnimatedNodes(nodesManager: NativeAnimatedNodesManager): SparseArray<AnimatedNode> {
106
- val allNodes = SparseArray<AnimatedNode>()
107
- try {
108
- // Access mAnimatedNodes field using reflection
109
- val animatedNodesField = findOrCacheField(nodesManager.javaClass, "mAnimatedNodes", "mAnimatedNodesField")
110
- @Suppress("UNCHECKED_CAST")
111
- val animatedNodes = animatedNodesField?.get(nodesManager) as? SparseArray<AnimatedNode>
112
- if (animatedNodes != null) {
113
- Log.d(LOG_TAG, "Found ${animatedNodes.size()} animated nodes in graph")
114
- for (i in 0 until animatedNodes.size()) {
115
- val node = animatedNodes.valueAt(i)
116
- allNodes.put(animatedNodes.keyAt(i), node)
117
- }
118
- } else {
119
- Log.w(LOG_TAG, "Could not access mAnimatedNodes field")
120
- }
121
- } catch (e: Exception) {
122
- Log.e(LOG_TAG, "Failed to get all animated nodes", e)
123
- }
124
- return allNodes
125
- }
126
-
127
- private fun findAnimatingNodes(nodesManager: NativeAnimatedNodesManager, allNodes: SparseArray<AnimatedNode>): Set<AnimatedNode> {
128
- val animatingNodes = mutableSetOf<AnimatedNode>()
129
-
130
- try {
131
- // Get nodes from active animations
132
- val activeAnimationsField = findOrCacheField(nodesManager.javaClass, "mActiveAnimations", "mActiveAnimationsField")
133
- @Suppress("UNCHECKED_CAST")
134
- val activeAnimations = activeAnimationsField?.get(nodesManager) as? SparseArray<Any>
135
- if (activeAnimations != null) {
136
- Log.d(LOG_TAG, "Found ${activeAnimations.size()} active animations")
137
- for (i in 0 until activeAnimations.size()) {
138
- val driver = activeAnimations.valueAt(i)
139
- Log.d(LOG_TAG, "Active animation driver: ${driver.javaClass.simpleName}")
140
- val animatedValueField = findOrCacheField(driver.javaClass, "animatedValue", "animatedValueField")
141
- val valueNode = animatedValueField?.get(driver) as? AnimatedNode
142
- if (valueNode != null) {
143
- animatingNodes.add(valueNode)
144
- Log.d(LOG_TAG, "Added animating node from active animation: ${valueNode.javaClass.simpleName}")
145
- }
146
- }
147
- }
148
-
149
- // Get nodes from updated nodes
150
- val updatedNodesField = findOrCacheField(nodesManager.javaClass, "mUpdatedNodes", "mUpdatedNodesField")
151
- @Suppress("UNCHECKED_CAST")
152
- val updatedNodes = updatedNodesField?.get(nodesManager) as? SparseArray<AnimatedNode>
153
- if (updatedNodes != null) {
154
- Log.d(LOG_TAG, "Found ${updatedNodes.size()} updated nodes")
155
- for (i in 0 until updatedNodes.size()) {
156
- val node = updatedNodes.valueAt(i)
157
- animatingNodes.add(node)
158
- Log.d(LOG_TAG, "Added updated node: ${node.javaClass.simpleName}")
159
- }
160
- }
161
- } catch (e: Exception) {
162
- Log.e(LOG_TAG, "Failed to find animating nodes", e)
163
- }
164
-
165
- return animatingNodes
166
- }
167
-
168
- private fun findPropsNodes(animatingNodes: Set<AnimatedNode>, allNodes: SparseArray<AnimatedNode>): Set<Any> {
169
- val allRelevantNodes = mutableSetOf<Any>()
170
- val queue = LinkedList<AnimatedNode>(animatingNodes)
171
- val visited = mutableSetOf<AnimatedNode>()
172
-
173
- while (queue.isNotEmpty()) {
174
- val node = queue.poll()
175
- if (node in visited) {
176
- continue
177
- }
178
- visited.add(node)
179
-
180
- // Check what type of node this is and log accordingly
181
- val nodeType = node.javaClass.simpleName
182
- when (nodeType) {
183
- "PropsAnimatedNode" -> {
184
- allRelevantNodes.add(node)
185
- Log.d(LOG_TAG, "Found PropsAnimatedNode: $nodeType")
186
- }
187
- "StyleAnimatedNode" -> {
188
- allRelevantNodes.add(node)
189
- Log.d(LOG_TAG, "Found StyleAnimatedNode: $nodeType")
190
- }
191
- "ValueAnimatedNode" -> {
192
- allRelevantNodes.add(node)
193
- Log.d(LOG_TAG, "Found ValueAnimatedNode: $nodeType")
194
- }
195
- else -> {
196
- Log.d(LOG_TAG, "Found other animated node: $nodeType")
197
- }
198
- }
199
-
200
- // Traverse children to find more nodes
201
- try {
202
- val childrenField = findOrCacheField(node.javaClass, "children", "childrenField")
203
- @Suppress("UNCHECKED_CAST")
204
- val children = childrenField?.get(node) as? List<AnimatedNode>
205
- if (children != null) {
206
- queue.addAll(children)
207
- }
208
- } catch (e: Exception) {
209
- // Ignored - not all nodes have children
210
- }
211
- }
212
-
213
- return allRelevantNodes
214
- }
215
-
216
- private fun isPropsAnimatedNode(node: AnimatedNode): Boolean {
217
- return try {
218
- // Check if this is actually a PropsAnimatedNode by checking the class name
219
- // and verifying it has the connectedViewTag field
220
- val isPropsNode = node.javaClass.simpleName == "PropsAnimatedNode"
221
- if (isPropsNode) {
222
- val connectedViewTagField = findOrCacheField(node.javaClass, "connectedViewTag", "connectedViewTagField")
223
- connectedViewTagField != null
224
- } else {
225
- false
226
- }
227
- } catch (e: Exception) {
228
- false
229
- }
230
- }
231
-
232
- private fun getViewTags(relevantNodes: Set<Any>, allNodes: SparseArray<AnimatedNode>): Set<Int> {
233
- val viewTags = mutableSetOf<Int>()
234
- for (node in relevantNodes) {
235
- try {
236
- val nodeType = node.javaClass.simpleName
237
- Log.d(LOG_TAG, "Processing $nodeType for view tags")
238
-
239
- when (nodeType) {
240
- "PropsAnimatedNode" -> {
241
- val connectedViewTagField = findOrCacheField(node.javaClass, "connectedViewTag", "connectedViewTagField")
242
- val viewTag = connectedViewTagField?.get(node) as? Int
243
- if (viewTag != null && viewTag != -1) {
244
- viewTags.add(viewTag)
245
- Log.d(LOG_TAG, "PropsAnimatedNode connected to view tag: $viewTag")
246
-
247
- // Log the property mapping to see what properties are being animated
248
- try {
249
- val propNodeMappingField = findOrCacheField(node.javaClass, "propNodeMapping", "propNodeMappingField")
250
- val propNodeMapping = propNodeMappingField?.get(node) as? Map<String, Int>
251
- if (propNodeMapping != null) {
252
- Log.d(LOG_TAG, "View $viewTag has animated properties: ${propNodeMapping.keys}")
253
- }
254
- } catch (e: Exception) {
255
- Log.d(LOG_TAG, "Could not access propNodeMapping for PropsAnimatedNode")
256
- }
257
- }
258
- }
259
- "StyleAnimatedNode" -> {
260
- // StyleAnimatedNode doesn't have connectedViewTag field - it's connected through PropsAnimatedNode
261
- Log.d(LOG_TAG, "StyleAnimatedNode has no direct view connection (connected through PropsAnimatedNode)")
262
-
263
- // Try to access propMapping to see what properties it handles
264
- try {
265
- val propMappingField = findOrCacheField(node.javaClass, "propMapping", "propMappingField")
266
- val propMapping = propMappingField?.get(node) as? Map<String, Int>
267
- if (propMapping != null) {
268
- Log.d(LOG_TAG, "StyleAnimatedNode handles properties: ${propMapping.keys}")
269
- }
270
- } catch (e: Exception) {
271
- Log.d(LOG_TAG, "Could not access StyleAnimatedNode propMapping: ${e.message}")
272
- }
273
-
274
- // Find the connected view by traversing the graph to find PropsAnimatedNode
275
- val connectedViewTag = findConnectedViewThroughGraph(node as AnimatedNode, allNodes)
276
- if (connectedViewTag != -1) {
277
- viewTags.add(connectedViewTag)
278
- Log.d(LOG_TAG, "StyleAnimatedNode connected to view tag through graph: $connectedViewTag")
279
- } else {
280
- Log.w(LOG_TAG, "StyleAnimatedNode has no connected view - this could mean:")
281
- Log.w(LOG_TAG, " 1. No PropsAnimatedNode found in graph traversal")
282
- Log.w(LOG_TAG, " 2. PropsAnimatedNode exists but not connected to any view")
283
- Log.w(LOG_TAG, " 3. Graph traversal failed due to reflection errors")
284
- Log.w(LOG_TAG, " 4. Circular references or disconnected graph")
285
- }
286
- }
287
- "ValueAnimatedNode" -> {
288
- // ValueAnimatedNode typically doesn't have direct view connection
289
- // but we can log its value for debugging
290
- try {
291
- val nodeValueField = findOrCacheField(node.javaClass, "nodeValue", "nodeValueField")
292
- val nodeValue = nodeValueField?.get(node) as? Double
293
- val offsetField = findOrCacheField(node.javaClass, "offset", "offsetField")
294
- val offset = offsetField?.get(node) as? Double
295
- Log.d(LOG_TAG, "ValueAnimatedNode value: $nodeValue, offset: $offset")
296
- } catch (e: Exception) {
297
- Log.d(LOG_TAG, "Could not access ValueAnimatedNode values: ${e.message}")
298
- }
299
- }
300
- else -> {
301
- Log.d(LOG_TAG, "Unknown node type: $nodeType")
302
- }
303
- }
304
- } catch (e: Exception) {
305
- Log.e(LOG_TAG, "Failed to process node: ${node.javaClass.simpleName}", e)
306
- }
307
- }
308
- return viewTags
309
- }
310
-
311
- private fun logViews(reactContext: ReactApplicationContext, viewTags: Set<Int>) {
312
- val uiManager = UIManagerHelper.getUIManager(reactContext, UIManagerType.FABRIC)
313
- if (uiManager == null) {
314
- Log.w(LOG_TAG, "Fabric UIManager not found.")
315
- return
316
- }
317
-
318
- for (tag in viewTags) {
319
- try {
320
- reactContext.runOnUiQueueThread {
321
- val view = uiManager.resolveView(tag)
322
- if (view != null) {
323
- ViewLifecycleRegistry.markAnimated(view)
324
-
325
- // Get view coordinates and dimensions
326
- val left = view.left
327
- val top = view.top
328
- val right = view.right
329
- val bottom = view.bottom
330
- val width = right - left
331
- val height = bottom - top
332
-
333
- Log.i(LOG_TAG, "Animating view: tag=$tag, class=${view.javaClass.simpleName}, id=${view.id}, " +
334
- "bounds=[$left,$top,$right,$bottom], size=${width}x${height}")
335
- } else {
336
- Log.w(LOG_TAG, "Could not resolve view for tag: $tag")
337
- }
338
- }
339
- } catch (e: Exception) {
340
- Log.e(LOG_TAG, "Failed to resolve or log view for tag: $tag", e)
341
- }
342
- }
343
- }
344
-
345
- private fun findOrCacheField(clazz: Class<*>, fieldName: String, cacheFieldName: String): Field? {
346
- try {
347
- val cacheField = FabricAnimationsInquirer::class.java.getDeclaredField(cacheFieldName).apply { isAccessible = true }
348
- var field = cacheField.get(this) as? Field
349
- if (field == null) {
350
- field = findFieldRecursive(clazz, fieldName)
351
- if (field != null) {
352
- cacheField.set(this, field)
353
- }
354
- }
355
- return field
356
- } catch (e: Exception) {
357
- Log.w(LOG_TAG, "Could not find or cache field $fieldName", e)
358
- return null
359
- }
360
- }
361
-
362
- private fun findFieldRecursive(clazz: Class<*>, fieldName: String): Field? {
363
- var currentClass: Class<*>? = clazz
364
- while (currentClass != null && currentClass != Any::class.java) {
365
- try {
366
- return currentClass.getDeclaredField(fieldName).apply { isAccessible = true }
367
- } catch (e: NoSuchFieldException) {
368
- // Not in this class, check superclass
369
- }
370
- currentClass = currentClass.superclass
371
- }
372
- Log.w(LOG_TAG, "Field '$fieldName' not found in class hierarchy for '${clazz.simpleName}'")
373
- return null
374
- }
375
-
376
- private fun findConnectedViewThroughGraph(startNode: AnimatedNode, allNodes: SparseArray<AnimatedNode>): Int {
377
- val queue = LinkedList<AnimatedNode>()
378
- val visited = mutableSetOf<AnimatedNode>()
379
- var propsNodesFound = 0
380
- var propsNodesWithView = 0
381
- var propsNodesWithoutView = 0
382
-
383
- Log.d(LOG_TAG, "Starting graph traversal from ${startNode.javaClass.simpleName}")
384
-
385
- queue.add(startNode)
386
- visited.add(startNode)
387
-
388
- while (queue.isNotEmpty()) {
389
- val node = queue.poll()
390
- val nodeType = node.javaClass.simpleName
391
-
392
- // Check if this is a PropsAnimatedNode with a connected view
393
- if (nodeType == "PropsAnimatedNode") {
394
- propsNodesFound++
395
- try {
396
- val connectedViewTagField = findOrCacheField(node.javaClass, "connectedViewTag", "connectedViewTagField")
397
- val viewTag = connectedViewTagField?.get(node) as? Int
398
- if (viewTag != null && viewTag != -1) {
399
- propsNodesWithView++
400
- Log.d(LOG_TAG, "Found PropsAnimatedNode with connected view: $viewTag")
401
- return viewTag
402
- } else {
403
- propsNodesWithoutView++
404
- Log.d(LOG_TAG, "Found PropsAnimatedNode but no connected view (viewTag: $viewTag)")
405
- }
406
- } catch (e: Exception) {
407
- Log.w(LOG_TAG, "Failed to access connectedViewTag from PropsAnimatedNode: ${e.message}")
408
- }
409
- }
410
-
411
- // Traverse children to find PropsAnimatedNode
412
- try {
413
- val childrenField = findOrCacheField(node.javaClass, "children", "childrenField")
414
- @Suppress("UNCHECKED_CAST")
415
- val children = childrenField?.get(node) as? List<AnimatedNode>
416
- if (children != null) {
417
- Log.d(LOG_TAG, "Traversing ${children.size} children from $nodeType")
418
- for (child in children) {
419
- if (child !in visited) {
420
- visited.add(child)
421
- queue.add(child)
422
- }
423
- }
424
- } else {
425
- Log.d(LOG_TAG, "$nodeType has no children")
426
- }
427
- } catch (e: Exception) {
428
- Log.d(LOG_TAG, "Could not access children from $nodeType: ${e.message}")
429
- }
430
- }
431
-
432
- Log.w(LOG_TAG, "Graph traversal completed:")
433
- Log.w(LOG_TAG, " - Total nodes visited: ${visited.size}")
434
- Log.w(LOG_TAG, " - PropsAnimatedNodes found: $propsNodesFound")
435
- Log.w(LOG_TAG, " - PropsAnimatedNodes with view: $propsNodesWithView")
436
- Log.w(LOG_TAG, " - PropsAnimatedNodes without view: $propsNodesWithoutView")
437
-
438
- return -1
439
- }
440
- }
@@ -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
- }