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.
- 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
- package/Detox-android/com/wix/detox/20.44.0/detox-20.44.0-sources.jar.md5 +1 -0
- package/Detox-android/com/wix/detox/20.44.0/detox-20.44.0-sources.jar.sha1 +1 -0
- package/Detox-android/com/wix/detox/20.44.0/detox-20.44.0-sources.jar.sha256 +1 -0
- package/Detox-android/com/wix/detox/20.44.0/detox-20.44.0-sources.jar.sha512 +1 -0
- package/Detox-android/com/wix/detox/20.44.0/detox-20.44.0.aar +0 -0
- package/Detox-android/com/wix/detox/20.44.0/detox-20.44.0.aar.md5 +1 -0
- package/Detox-android/com/wix/detox/20.44.0/detox-20.44.0.aar.sha1 +1 -0
- package/Detox-android/com/wix/detox/20.44.0/detox-20.44.0.aar.sha256 +1 -0
- package/Detox-android/com/wix/detox/20.44.0/detox-20.44.0.aar.sha512 +1 -0
- 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
- package/Detox-android/com/wix/detox/20.44.0/detox-20.44.0.pom.md5 +1 -0
- package/Detox-android/com/wix/detox/20.44.0/detox-20.44.0.pom.sha1 +1 -0
- package/Detox-android/com/wix/detox/20.44.0/detox-20.44.0.pom.sha256 +1 -0
- package/Detox-android/com/wix/detox/20.44.0/detox-20.44.0.pom.sha512 +1 -0
- package/Detox-android/com/wix/detox/maven-metadata.xml +4 -4
- package/Detox-android/com/wix/detox/maven-metadata.xml.md5 +1 -1
- package/Detox-android/com/wix/detox/maven-metadata.xml.sha1 +1 -1
- package/Detox-android/com/wix/detox/maven-metadata.xml.sha256 +1 -1
- package/Detox-android/com/wix/detox/maven-metadata.xml.sha512 +1 -1
- package/Detox-ios-framework.tbz +0 -0
- package/Detox-ios-src.tbz +0 -0
- package/Detox-ios-xcuitest.tbz +0 -0
- package/android/detox/proguard-rules-app.pro +1 -0
- package/android/detox/src/full/java/com/wix/detox/common/KotlinReflectUtils.kt +31 -0
- package/android/detox/src/full/java/com/wix/detox/espresso/hierarchy/ViewHierarchyGenerator.kt +0 -15
- package/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/animations/AnimatedModuleIdlingResource.kt +3 -20
- package/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/timers/JavaTimersReflected.kt +8 -1
- package/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/fabric/FabricUIManagerIdlingResources.kt +14 -3
- package/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/uimodule/paper/UIModuleIdlingResource.kt +1 -1
- package/android/detox/src/full/java/com/wix/detox/reactnative/ui/UIExtensions.kt +7 -3
- package/android/detox/src/testFull/java/com/wix/detox/common/UIExtensionsTest.kt +3 -1
- package/android/gradle/wrapper/gradle-wrapper.properties +1 -1
- package/android/rninfo.gradle +1 -0
- package/package.json +11 -11
- package/scripts/updateGradle.js +1 -1
- package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4-sources.jar.md5 +0 -1
- package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4-sources.jar.sha1 +0 -1
- package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4-sources.jar.sha256 +0 -1
- package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4-sources.jar.sha512 +0 -1
- package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4.aar +0 -0
- package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4.aar.md5 +0 -1
- package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4.aar.sha1 +0 -1
- package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4.aar.sha256 +0 -1
- package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4.aar.sha512 +0 -1
- package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4.pom.md5 +0 -1
- package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4.pom.sha1 +0 -1
- package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4.pom.sha256 +0 -1
- package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4.pom.sha512 +0 -1
- package/android/detox/src/full/java/com/wix/detox/inquiry/FabricAnimationsInquirer.kt +0 -629
- package/android/detox/src/full/java/com/wix/detox/inquiry/ViewLifecycleRegistry.kt +0 -143
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
8742023668a8e532cf18692cad091938
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ad0bb17a22af68dececf00e90f2b6ddb0faade67
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cb9a4c6ad50b011b5ac72ffb30b66866af988d647bcffd645b3f38f589add79e
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
532b3de4c23855a7bf62dbddf8406e5656bf81a3435a39adad9645fd019b679c0e16e699ce1e8085c9401dda8e8ffa661904e586f3c3897eddf7e143ae06dbbe
|
|
Binary file
|
|
@@ -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
|
|
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
|
|
7
|
-
<release>20.44.0
|
|
6
|
+
<latest>20.44.0</latest>
|
|
7
|
+
<release>20.44.0</release>
|
|
8
8
|
<versions>
|
|
9
|
-
<version>20.44.0
|
|
9
|
+
<version>20.44.0</version>
|
|
10
10
|
</versions>
|
|
11
|
-
<lastUpdated>
|
|
11
|
+
<lastUpdated>20251019160654</lastUpdated>
|
|
12
12
|
</versioning>
|
|
13
13
|
</metadata>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
129e8845e2c49ebb6a8a08917ba2f5ca
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
5155667329c4ec88cdebcd3eec1e00ab5cc18aab
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
3141cb84e424059e9ed45660a82052b581406ab15a4fc8c400df3c1950fae08c
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
81198b46c3596f99eb9a5ed2536ed0cc18594792109aec6e36e91bc4be60f5cc5750b98c4add46ecc407ac4502f7c7fb66313f710c673ddfcec10f13bcb99ac9
|
package/Detox-ios-framework.tbz
CHANGED
|
Binary file
|
package/Detox-ios-src.tbz
CHANGED
|
Binary file
|
package/Detox-ios-xcuitest.tbz
CHANGED
|
Binary file
|
|
@@ -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
|
+
}
|
package/android/detox/src/full/java/com/wix/detox/espresso/hierarchy/ViewHierarchyGenerator.kt
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
118
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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 = "
|
|
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 (
|
|
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 =
|
|
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.
|
|
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
|
package/android/rninfo.gradle
CHANGED
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
|
|
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": "
|
|
38
|
-
"@react-native-community/cli-platform-android": "
|
|
39
|
-
"@react-native-community/cli-platform-ios": "
|
|
40
|
-
"@react-native/babel-preset": "0.
|
|
41
|
-
"@react-native/eslint-config": "0.
|
|
42
|
-
"@react-native/metro-config": "0.
|
|
43
|
-
"@react-native/typescript-config": "0.
|
|
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.
|
|
65
|
+
"react-native": "0.82.0",
|
|
66
66
|
"react-native-codegen": "^0.0.8",
|
|
67
|
-
"typescript": "
|
|
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": "
|
|
123
|
+
"gitHead": "fb5940dbdf93e56185fed10f4adefecff37fcc77"
|
|
124
124
|
}
|
package/scripts/updateGradle.js
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1c1260d5d5dd7ba851908271b86c49c8
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
f5fd22fc1bec6c426f5dbcfa7d57bcb3a2663583
|
package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4-sources.jar.sha256
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
d0dcf3a7c33b5aeb99f910b1e264e3979c679531884ac40e3b8c00864890c253
|
package/Detox-android/com/wix/detox/20.44.0-smoke.4/detox-20.44.0-smoke.4-sources.jar.sha512
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
f096a65c66dc7241116db2c1dbd5cf1c86553e4a919b4e7ac9c61d618d25a0a5dedd7170fcd55dbf041a252856a32e124c5958eaa52d8aadaedc7c8adf6d0f44
|
|
Binary file
|
|
@@ -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
|
-
}
|