@datadog/mobile-react-native-session-replay 2.6.1 → 2.6.2
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/README.md +3 -3
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt +5 -5
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupport.kt +5 -29
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ShadowNodeWrapper.kt +2 -2
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactEditTextMapper.kt +3 -26
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactTextMapper.kt +3 -25
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/text/FabricTextViewUtils.kt +74 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/text/LegacyTextViewUtils.kt +118 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/{ReactTextPropertiesResolver.kt → utils/text/TextViewUtils.kt} +60 -96
- package/android/src/rn75/kotlin/com/datadog/reactnative/sessionreplay/extensions/LengthPercentageExt.kt +1 -2
- package/android/src/rn75/kotlin/com/datadog/reactnative/sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt +1 -3
- package/android/src/rn76/kotlin/com/datadog/reactnative/sessionreplay/extensions/LengthPercentageExt.kt +1 -1
- package/android/src/rn76/kotlin/com/datadog/reactnative/sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt +1 -3
- package/android/src/rnlegacy/kotlin/com.datadog.reactnative.sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt +2 -7
- package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupportTest.kt +3 -17
- package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/{ReactTextPropertiesResolverTest.kt → utils/text/TextViewUtilsTest.kt} +171 -38
- package/ios/Sources/DdSessionReplay.mm +4 -4
- package/ios/Sources/DdSessionReplayImplementation.swift +13 -3
- package/ios/Sources/RCTFabricWrapper.h +13 -0
- package/ios/Sources/RCTFabricWrapper.mm +120 -0
- package/ios/Sources/RCTTextPropertiesWrapper.h +23 -0
- package/ios/Sources/RCTTextPropertiesWrapper.mm +28 -0
- package/ios/Sources/RCTTextViewRecorder.swift +69 -49
- package/ios/Sources/RCTVersion.h +8 -0
- package/package.json +5 -3
- package/scripts/set-ios-rn-version.js +47 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/NoopTextPropertiesResolver.kt +0 -22
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/TextViewUtils.kt +0 -40
- package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/utils/TextViewUtilsTest.kt +0 -109
|
@@ -1,33 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
|
|
3
|
-
* This product includes software developed at Datadog (https://www.datadoghq.com/).
|
|
4
|
-
* Copyright 2016-Present Datadog, Inc.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
package com.datadog.reactnative.sessionreplay
|
|
1
|
+
package com.datadog.reactnative.sessionreplay.utils.text
|
|
8
2
|
|
|
9
3
|
import ReactViewBackgroundDrawableUtils
|
|
10
4
|
import android.view.Gravity
|
|
11
5
|
import android.widget.TextView
|
|
12
6
|
import androidx.annotation.VisibleForTesting
|
|
13
|
-
import com.datadog.android.
|
|
7
|
+
import com.datadog.android.api.InternalLogger
|
|
14
8
|
import com.datadog.android.sessionreplay.model.MobileSegment
|
|
9
|
+
import com.datadog.android.sessionreplay.recorder.MappingContext
|
|
10
|
+
import com.datadog.reactnative.sessionreplay.BuildConfig
|
|
15
11
|
import com.datadog.reactnative.sessionreplay.utils.DrawableUtils
|
|
16
12
|
import com.datadog.reactnative.sessionreplay.utils.ReflectionUtils
|
|
17
|
-
import com.datadog.reactnative.sessionreplay.utils.formatAsRgba
|
|
18
13
|
import com.facebook.react.bridge.ReactContext
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
)
|
|
30
|
-
|
|
14
|
+
|
|
15
|
+
internal abstract class TextViewUtils(private val reactContext: ReactContext, private val drawableUtils: DrawableUtils) {
|
|
16
|
+
fun mapTextViewToWireframes(
|
|
17
|
+
wireframes: List<MobileSegment.Wireframe>,
|
|
18
|
+
view: TextView,
|
|
19
|
+
mappingContext: MappingContext,
|
|
20
|
+
): List<MobileSegment.Wireframe> {
|
|
21
|
+
val result = mutableListOf<MobileSegment.Wireframe>()
|
|
22
|
+
val pixelDensity = mappingContext.systemInformation.screenDensity
|
|
23
|
+
|
|
24
|
+
for (originalWireframe in wireframes) {
|
|
25
|
+
if (originalWireframe !is MobileSegment.Wireframe.TextWireframe) {
|
|
26
|
+
result.add(originalWireframe)
|
|
27
|
+
} else {
|
|
28
|
+
result.add(addReactNativeProperties(
|
|
29
|
+
originalWireframe = originalWireframe,
|
|
30
|
+
view = view,
|
|
31
|
+
pixelDensity = pixelDensity,
|
|
32
|
+
))
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return result
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fun addReactNativeProperties(
|
|
31
40
|
originalWireframe: MobileSegment.Wireframe.TextWireframe,
|
|
32
41
|
view: TextView,
|
|
33
42
|
pixelDensity: Float,
|
|
@@ -59,25 +68,17 @@ internal class ReactTextPropertiesResolver(
|
|
|
59
68
|
)
|
|
60
69
|
}
|
|
61
70
|
|
|
62
|
-
|
|
71
|
+
|
|
72
|
+
protected fun resolveTextStyleAndPosition(
|
|
63
73
|
originalWireframe: MobileSegment.Wireframe.TextWireframe,
|
|
64
74
|
view: TextView,
|
|
65
|
-
pixelDensity: Float
|
|
66
|
-
):
|
|
67
|
-
Pair<MobileSegment.TextStyle, MobileSegment.TextPosition>? {
|
|
68
|
-
|
|
75
|
+
pixelDensity: Float
|
|
76
|
+
): Pair<MobileSegment.TextStyle, MobileSegment.TextPosition>? {
|
|
69
77
|
if (!reactContext.hasActiveReactInstance()) {
|
|
70
78
|
return null
|
|
71
79
|
}
|
|
72
80
|
|
|
73
|
-
val
|
|
74
|
-
ShadowNodeWrapper.getShadowNodeWrapper(
|
|
75
|
-
reactContext = reactContext,
|
|
76
|
-
uiManagerModule = uiManagerModule,
|
|
77
|
-
reflectionUtils = reflectionUtils,
|
|
78
|
-
viewId = view.id) ?: return null
|
|
79
|
-
|
|
80
|
-
val textStyle = resolveTextStyle(originalWireframe, pixelDensity, shadowNodeWrapper)
|
|
81
|
+
val textStyle = resolveTextStyle(originalWireframe, pixelDensity, view) ?: return null
|
|
81
82
|
val alignment = resolveTextAlignment(view, originalWireframe)
|
|
82
83
|
|
|
83
84
|
val textPosition = MobileSegment.TextPosition(
|
|
@@ -88,7 +89,7 @@ internal class ReactTextPropertiesResolver(
|
|
|
88
89
|
return textStyle to textPosition
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
|
|
92
|
+
protected fun resolveShapeStyleAndBorder(
|
|
92
93
|
view: TextView,
|
|
93
94
|
pixelDensity: Float,
|
|
94
95
|
): Pair<MobileSegment.ShapeStyle?, MobileSegment.ShapeBorder?>? {
|
|
@@ -105,7 +106,7 @@ internal class ReactTextPropertiesResolver(
|
|
|
105
106
|
return shapeStyle to border
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
|
|
109
|
+
protected fun resolveTextAlignment(
|
|
109
110
|
view: TextView,
|
|
110
111
|
textWireframe: MobileSegment.Wireframe.TextWireframe
|
|
111
112
|
): MobileSegment.Alignment {
|
|
@@ -126,64 +127,7 @@ internal class ReactTextPropertiesResolver(
|
|
|
126
127
|
)
|
|
127
128
|
}
|
|
128
129
|
|
|
129
|
-
|
|
130
|
-
textWireframe: MobileSegment.Wireframe.TextWireframe,
|
|
131
|
-
pixelsDensity: Float,
|
|
132
|
-
shadowNodeWrapper: ShadowNodeWrapper
|
|
133
|
-
): MobileSegment.TextStyle {
|
|
134
|
-
val fontFamily = getFontFamily(shadowNodeWrapper)
|
|
135
|
-
?: textWireframe.textStyle.family
|
|
136
|
-
val fontSize = getFontSize(shadowNodeWrapper)
|
|
137
|
-
?.densityNormalized(pixelsDensity)
|
|
138
|
-
?: textWireframe.textStyle.size
|
|
139
|
-
val fontColor = getTextColor(shadowNodeWrapper)
|
|
140
|
-
?: textWireframe.textStyle.color
|
|
141
|
-
|
|
142
|
-
return MobileSegment.TextStyle(
|
|
143
|
-
family = fontFamily,
|
|
144
|
-
size = fontSize,
|
|
145
|
-
color = fontColor
|
|
146
|
-
)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
private fun getTextColor(shadowNodeWrapper: ShadowNodeWrapper): String? {
|
|
150
|
-
val isColorSet = shadowNodeWrapper
|
|
151
|
-
.getDeclaredShadowNodeField(IS_COLOR_SET_FIELD_NAME) as Boolean?
|
|
152
|
-
if (isColorSet != true) {
|
|
153
|
-
// Improvement: get default text color if different from black
|
|
154
|
-
return "#000000FF"
|
|
155
|
-
}
|
|
156
|
-
val resolvedColor = shadowNodeWrapper
|
|
157
|
-
.getDeclaredShadowNodeField(COLOR_FIELD_NAME) as Int?
|
|
158
|
-
if (resolvedColor != null) {
|
|
159
|
-
return formatAsRgba(resolvedColor)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return null
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
private fun getFontSize(shadowNodeWrapper: ShadowNodeWrapper): Long? {
|
|
166
|
-
val textAttributes = shadowNodeWrapper
|
|
167
|
-
.getDeclaredShadowNodeField(TEXT_ATTRIBUTES_FIELD_NAME) as? TextAttributes?
|
|
168
|
-
if (textAttributes != null) {
|
|
169
|
-
return textAttributes.effectiveFontSize.toLong()
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return null
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
private fun getFontFamily(shadowNodeWrapper: ShadowNodeWrapper): String? {
|
|
176
|
-
val fontFamily = shadowNodeWrapper
|
|
177
|
-
.getDeclaredShadowNodeField(FONT_FAMILY_FIELD_NAME) as? String
|
|
178
|
-
|
|
179
|
-
if (fontFamily != null) {
|
|
180
|
-
return resolveFontFamily(fontFamily.lowercase(Locale.US))
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return null
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
private fun resolveFontFamily(typefaceName: String): String =
|
|
130
|
+
protected fun resolveFontFamily(typefaceName: String): String =
|
|
187
131
|
when (typefaceName) {
|
|
188
132
|
ROBOTO_TYPEFACE_NAME -> SANS_SERIF_FAMILY_NAME
|
|
189
133
|
MONOSPACE_FAMILY_NAME -> MONOSPACE_FAMILY_NAME
|
|
@@ -191,16 +135,36 @@ internal class ReactTextPropertiesResolver(
|
|
|
191
135
|
else -> SANS_SERIF_FAMILY_NAME
|
|
192
136
|
}
|
|
193
137
|
|
|
138
|
+
protected abstract fun resolveTextStyle( textWireframe: MobileSegment.Wireframe.TextWireframe,
|
|
139
|
+
pixelsDensity: Float,
|
|
140
|
+
view: TextView
|
|
141
|
+
): MobileSegment.TextStyle?
|
|
142
|
+
|
|
194
143
|
@VisibleForTesting
|
|
195
|
-
|
|
144
|
+
companion object {
|
|
196
145
|
internal const val TEXT_ATTRIBUTES_FIELD_NAME = "mTextAttributes"
|
|
197
146
|
internal const val FONT_FAMILY_FIELD_NAME = "mFontFamily"
|
|
198
147
|
internal const val COLOR_FIELD_NAME = "mColor"
|
|
199
148
|
internal const val IS_COLOR_SET_FIELD_NAME = "mIsColorSet"
|
|
149
|
+
internal const val SPANNED_FIELD_NAME = "mSpanned"
|
|
200
150
|
|
|
201
151
|
private const val ROBOTO_TYPEFACE_NAME = "roboto"
|
|
202
152
|
private const val SERIF_FAMILY_NAME = "serif"
|
|
203
153
|
private const val SANS_SERIF_FAMILY_NAME = "roboto, sans-serif"
|
|
204
154
|
internal const val MONOSPACE_FAMILY_NAME = "monospace"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
internal const val RESOLVE_UIMANAGERMODULE_ERROR = "Unable to resolve UIManagerModule"
|
|
158
|
+
internal const val RESOLVE_FABRICFIELD_ERROR = "Unable to resolve field from fabric view"
|
|
159
|
+
internal const val NULL_FABRICFIELD_ERROR = "Null value found when trying to resolve field from fabric view"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
fun create(reactContext: ReactContext, logger: InternalLogger): TextViewUtils {
|
|
163
|
+
return when (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
|
164
|
+
true -> FabricTextViewUtils(reactContext, logger, ReactViewBackgroundDrawableUtils())
|
|
165
|
+
false -> LegacyTextViewUtils(reactContext, logger, ReflectionUtils(), ReactViewBackgroundDrawableUtils())
|
|
166
|
+
}
|
|
167
|
+
}
|
|
205
168
|
}
|
|
206
|
-
|
|
169
|
+
|
|
170
|
+
}
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import android.graphics.drawable.Drawable
|
|
7
7
|
import android.graphics.drawable.InsetDrawable
|
|
8
8
|
import android.graphics.drawable.LayerDrawable
|
|
9
|
-
import com.datadog.android.internal.utils.densityNormalized
|
|
10
9
|
import com.datadog.android.sessionreplay.model.MobileSegment
|
|
11
10
|
import com.datadog.reactnative.sessionreplay.extensions.getRadius
|
|
12
11
|
import com.datadog.reactnative.sessionreplay.utils.DrawableUtils
|
|
@@ -85,8 +84,7 @@ internal class ReactViewBackgroundDrawableUtils : DrawableUtils() {
|
|
|
85
84
|
backgroundDrawable: CSSBackgroundDrawable,
|
|
86
85
|
pixelDensity: Float
|
|
87
86
|
): MobileSegment.ShapeBorder {
|
|
88
|
-
val borderWidth =
|
|
89
|
-
backgroundDrawable.fullBorderWidth.toLong().densityNormalized(pixelDensity)
|
|
87
|
+
val borderWidth = (backgroundDrawable.fullBorderWidth / pixelDensity).toLong()
|
|
90
88
|
val borderColor = formatAsRgba(backgroundDrawable.getBorderColor(Spacing.ALL))
|
|
91
89
|
|
|
92
90
|
return MobileSegment.ShapeBorder(
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import android.graphics.drawable.Drawable
|
|
7
7
|
import android.graphics.drawable.InsetDrawable
|
|
8
8
|
import android.graphics.drawable.LayerDrawable
|
|
9
|
-
import com.datadog.android.internal.utils.densityNormalized
|
|
10
9
|
import com.datadog.android.sessionreplay.model.MobileSegment
|
|
11
10
|
import com.datadog.reactnative.sessionreplay.extensions.getRadius
|
|
12
11
|
import com.datadog.reactnative.sessionreplay.utils.DrawableUtils
|
|
@@ -85,8 +84,7 @@ internal class ReactViewBackgroundDrawableUtils : DrawableUtils() {
|
|
|
85
84
|
backgroundDrawable: CSSBackgroundDrawable,
|
|
86
85
|
pixelDensity: Float
|
|
87
86
|
): MobileSegment.ShapeBorder {
|
|
88
|
-
val borderWidth =
|
|
89
|
-
backgroundDrawable.fullBorderWidth.toLong().densityNormalized(pixelDensity)
|
|
87
|
+
val borderWidth = (backgroundDrawable.fullBorderWidth / pixelDensity).toLong()
|
|
90
88
|
val borderColor = formatAsRgba(backgroundDrawable.getBorderColor(Spacing.ALL))
|
|
91
89
|
|
|
92
90
|
return MobileSegment.ShapeBorder(
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import android.graphics.drawable.Drawable
|
|
2
2
|
import android.graphics.drawable.InsetDrawable
|
|
3
3
|
import android.graphics.drawable.LayerDrawable
|
|
4
|
-
import com.datadog.android.internal.utils.densityNormalized
|
|
5
4
|
import com.datadog.android.sessionreplay.model.MobileSegment
|
|
6
5
|
import com.datadog.reactnative.sessionreplay.utils.DrawableUtils
|
|
7
6
|
import com.datadog.reactnative.sessionreplay.utils.formatAsRgba
|
|
@@ -19,10 +18,7 @@ internal class ReactViewBackgroundDrawableUtils() : DrawableUtils() {
|
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
val borderProps = resolveBorder(drawable, pixelDensity)
|
|
22
|
-
val cornerRadius = drawable
|
|
23
|
-
.fullBorderRadius
|
|
24
|
-
.toLong()
|
|
25
|
-
.densityNormalized(pixelDensity)
|
|
21
|
+
val cornerRadius = (drawable.fullBorderRadius / pixelDensity).toLong()
|
|
26
22
|
|
|
27
23
|
val backgroundColor = getBackgroundColor(drawable)
|
|
28
24
|
val colorHexString = if (backgroundColor != null) {
|
|
@@ -63,8 +59,7 @@ internal class ReactViewBackgroundDrawableUtils() : DrawableUtils() {
|
|
|
63
59
|
backgroundDrawable: ReactViewBackgroundDrawable,
|
|
64
60
|
pixelDensity: Float
|
|
65
61
|
): MobileSegment.ShapeBorder {
|
|
66
|
-
val borderWidth =
|
|
67
|
-
backgroundDrawable.fullBorderWidth.toLong().densityNormalized(pixelDensity)
|
|
62
|
+
val borderWidth = (backgroundDrawable.fullBorderWidth / pixelDensity).toLong()
|
|
68
63
|
val borderColor = formatAsRgba(backgroundDrawable.getBorderColor(Spacing.ALL))
|
|
69
64
|
|
|
70
65
|
return MobileSegment.ShapeBorder(
|
|
@@ -11,6 +11,7 @@ import com.datadog.reactnative.sessionreplay.mappers.ReactEditTextMapper
|
|
|
11
11
|
import com.datadog.reactnative.sessionreplay.mappers.ReactNativeImageViewMapper
|
|
12
12
|
import com.datadog.reactnative.sessionreplay.mappers.ReactTextMapper
|
|
13
13
|
import com.datadog.reactnative.sessionreplay.mappers.ReactViewGroupMapper
|
|
14
|
+
import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils
|
|
14
15
|
import com.facebook.react.bridge.NativeModule
|
|
15
16
|
import com.facebook.react.bridge.ReactContext
|
|
16
17
|
import com.facebook.react.uimanager.UIManagerModule
|
|
@@ -51,10 +52,8 @@ internal class ReactNativeSessionReplayExtensionSupportTest {
|
|
|
51
52
|
whenever(mockReactContext.getNativeModule(any<Class<NativeModule>>()))
|
|
52
53
|
.doReturn(mockUiManagerModule)
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
reactContext = mockReactContext
|
|
57
|
-
)
|
|
55
|
+
val textViewUtils = TextViewUtils.create(mockReactContext, mockLogger)
|
|
56
|
+
testedExtensionSupport = ReactNativeSessionReplayExtensionSupport(textViewUtils)
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
@Test
|
|
@@ -77,17 +76,4 @@ internal class ReactNativeSessionReplayExtensionSupportTest {
|
|
|
77
76
|
assertThat(customViewMappers[3].getUnsafeMapper())
|
|
78
77
|
.isInstanceOf(ReactEditTextMapper::class.java)
|
|
79
78
|
}
|
|
80
|
-
|
|
81
|
-
@Test
|
|
82
|
-
fun `M return null W getUiManagerModule() { cannot get uiManagerModule }`() {
|
|
83
|
-
// Given
|
|
84
|
-
whenever(mockReactContext.getNativeModule(any<Class<NativeModule>>()))
|
|
85
|
-
.thenThrow(IllegalStateException())
|
|
86
|
-
|
|
87
|
-
// When
|
|
88
|
-
val uiManagerModule = testedExtensionSupport.getUiManagerModule()
|
|
89
|
-
|
|
90
|
-
// Then
|
|
91
|
-
assertThat(uiManagerModule).isNull()
|
|
92
|
-
}
|
|
93
79
|
}
|
|
@@ -4,20 +4,30 @@
|
|
|
4
4
|
* Copyright 2016-Present Datadog, Inc.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
package com.datadog.reactnative.sessionreplay
|
|
7
|
+
package com.datadog.reactnative.sessionreplay.utils.text
|
|
8
8
|
|
|
9
|
+
import android.content.res.Resources
|
|
10
|
+
import android.graphics.Typeface
|
|
11
|
+
import android.text.Spannable
|
|
12
|
+
import android.text.style.ForegroundColorSpan
|
|
13
|
+
import android.util.DisplayMetrics
|
|
9
14
|
import android.widget.TextView
|
|
15
|
+
import com.datadog.android.api.InternalLogger
|
|
10
16
|
import com.datadog.android.sessionreplay.model.MobileSegment
|
|
11
|
-
import com.datadog.
|
|
12
|
-
import com.datadog.
|
|
13
|
-
import com.datadog.reactnative.sessionreplay.
|
|
14
|
-
import com.datadog.reactnative.sessionreplay.ReactTextPropertiesResolver.Companion.MONOSPACE_FAMILY_NAME
|
|
15
|
-
import com.datadog.reactnative.sessionreplay.ReactTextPropertiesResolver.Companion.TEXT_ATTRIBUTES_FIELD_NAME
|
|
17
|
+
import com.datadog.android.sessionreplay.recorder.MappingContext
|
|
18
|
+
import com.datadog.android.sessionreplay.recorder.SystemInformation
|
|
19
|
+
import com.datadog.reactnative.sessionreplay.ShadowNodeWrapper
|
|
16
20
|
import com.datadog.reactnative.sessionreplay.ShadowNodeWrapper.Companion.UI_IMPLEMENTATION_FIELD_NAME
|
|
17
21
|
import com.datadog.reactnative.sessionreplay.utils.DrawableUtils
|
|
18
22
|
import com.datadog.reactnative.sessionreplay.utils.ReflectionUtils
|
|
19
23
|
import com.datadog.reactnative.sessionreplay.utils.formatAsRgba
|
|
24
|
+
import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils.Companion.COLOR_FIELD_NAME
|
|
25
|
+
import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils.Companion.FONT_FAMILY_FIELD_NAME
|
|
26
|
+
import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils.Companion.IS_COLOR_SET_FIELD_NAME
|
|
27
|
+
import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils.Companion.MONOSPACE_FAMILY_NAME
|
|
28
|
+
import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils.Companion.TEXT_ATTRIBUTES_FIELD_NAME
|
|
20
29
|
import com.datadog.reactnative.tools.unit.forge.ForgeConfigurator
|
|
30
|
+
import com.facebook.react.bridge.NativeModule
|
|
21
31
|
import com.facebook.react.bridge.ReactContext
|
|
22
32
|
import com.facebook.react.uimanager.ReactShadowNode
|
|
23
33
|
import com.facebook.react.uimanager.UIImplementation
|
|
@@ -34,7 +44,11 @@ import org.junit.jupiter.api.BeforeEach
|
|
|
34
44
|
import org.junit.jupiter.api.Test
|
|
35
45
|
import org.junit.jupiter.api.extension.ExtendWith
|
|
36
46
|
import org.junit.jupiter.api.extension.Extensions
|
|
47
|
+
import org.mockito.ArgumentMatchers.anyInt
|
|
37
48
|
import org.mockito.Mock
|
|
49
|
+
import org.mockito.Mockito.doReturn
|
|
50
|
+
import org.mockito.Mockito.mock
|
|
51
|
+
import org.mockito.Mockito.spy
|
|
38
52
|
import org.mockito.junit.jupiter.MockitoExtension
|
|
39
53
|
import org.mockito.junit.jupiter.MockitoSettings
|
|
40
54
|
import org.mockito.kotlin.any
|
|
@@ -48,9 +62,7 @@ import org.mockito.quality.Strictness
|
|
|
48
62
|
)
|
|
49
63
|
@MockitoSettings(strictness = Strictness.LENIENT)
|
|
50
64
|
@ForgeConfiguration(ForgeConfigurator::class)
|
|
51
|
-
internal class
|
|
52
|
-
private lateinit var testedResolver: ReactTextPropertiesResolver
|
|
53
|
-
|
|
65
|
+
internal class TextViewUtilsTest {
|
|
54
66
|
@Mock
|
|
55
67
|
lateinit var mockReactContext: ReactContext
|
|
56
68
|
|
|
@@ -84,10 +96,44 @@ internal class ReactTextPropertiesResolverTest {
|
|
|
84
96
|
@Mock
|
|
85
97
|
private lateinit var mockShadowNode: ReactShadowNode<out ReactShadowNode<*>>
|
|
86
98
|
|
|
99
|
+
@Mock
|
|
100
|
+
private lateinit var mockLogger: InternalLogger
|
|
101
|
+
|
|
102
|
+
@Mock
|
|
103
|
+
private lateinit var mockMappingContext: MappingContext
|
|
104
|
+
|
|
105
|
+
@Mock
|
|
106
|
+
private lateinit var mockSystemInformation: SystemInformation
|
|
107
|
+
|
|
108
|
+
@Mock
|
|
109
|
+
private lateinit var mockResources: Resources
|
|
110
|
+
|
|
111
|
+
@Mock
|
|
112
|
+
private lateinit var mockDisplayMetrics: DisplayMetrics
|
|
113
|
+
|
|
114
|
+
@Mock
|
|
115
|
+
private lateinit var testedUtils: LegacyTextViewUtils
|
|
116
|
+
|
|
117
|
+
@Mock
|
|
118
|
+
private lateinit var fabricTestedUtils: FabricTextViewUtils
|
|
119
|
+
|
|
87
120
|
@BeforeEach
|
|
88
121
|
fun `set up`(forge: Forge) {
|
|
122
|
+
whenever(mockResources.displayMetrics).thenReturn(mockDisplayMetrics)
|
|
123
|
+
whenever(mockTextView.resources).thenReturn(mockResources)
|
|
124
|
+
whenever(mockSystemInformation.screenDensity).thenReturn(0f)
|
|
125
|
+
whenever(mockMappingContext.systemInformation).thenReturn(mockSystemInformation)
|
|
126
|
+
whenever(mockTextView.text).thenReturn(forge.aString())
|
|
127
|
+
whenever(mockTextView.typeface).thenReturn(Typeface.SANS_SERIF)
|
|
128
|
+
|
|
129
|
+
whenever(mockReactContext.getNativeModule(UIManagerModule::class.java))
|
|
130
|
+
.thenReturn(mockUiManagerModule)
|
|
131
|
+
|
|
89
132
|
whenever(
|
|
90
|
-
mockReflectionUtils.getDeclaredField(
|
|
133
|
+
mockReflectionUtils.getDeclaredField(
|
|
134
|
+
eq(mockUiManagerModule),
|
|
135
|
+
eq(UI_IMPLEMENTATION_FIELD_NAME)
|
|
136
|
+
)
|
|
91
137
|
).thenReturn(mockUiImplementation)
|
|
92
138
|
|
|
93
139
|
whenever(
|
|
@@ -101,12 +147,64 @@ internal class ReactTextPropertiesResolverTest {
|
|
|
101
147
|
}
|
|
102
148
|
whenever(mockReactContext.hasActiveReactInstance()).thenReturn(true)
|
|
103
149
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
150
|
+
val realUtils =
|
|
151
|
+
LegacyTextViewUtils(
|
|
152
|
+
mockReactContext,
|
|
153
|
+
mockLogger,
|
|
154
|
+
mockReflectionUtils,
|
|
155
|
+
mockDrawableUtils
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
val realFabricUtils =
|
|
159
|
+
FabricTextViewUtils(
|
|
160
|
+
mockReactContext,
|
|
161
|
+
mockLogger,
|
|
162
|
+
mockDrawableUtils
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
testedUtils = spy(realUtils)
|
|
166
|
+
fabricTestedUtils = spy(realFabricUtils)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@Test
|
|
170
|
+
fun `M return wireframe W map() { even if not TextWireframeType }`(
|
|
171
|
+
@Mock mockImageWireframe: MobileSegment.Wireframe.ImageWireframe
|
|
172
|
+
) {
|
|
173
|
+
// When
|
|
174
|
+
val result =
|
|
175
|
+
testedUtils.mapTextViewToWireframes(
|
|
176
|
+
wireframes = listOf(mockImageWireframe),
|
|
177
|
+
view = mockTextView,
|
|
178
|
+
mappingContext = mockMappingContext
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
// Then
|
|
182
|
+
assertThat(result).contains(mockImageWireframe)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@Test
|
|
186
|
+
fun `M return textWireframe W map()`(
|
|
187
|
+
@Mock mockTextWireframe: MobileSegment.Wireframe.TextWireframe
|
|
188
|
+
) {
|
|
189
|
+
// Given
|
|
190
|
+
doReturn(mockTextWireframe)
|
|
191
|
+
.whenever(testedUtils)
|
|
192
|
+
.addReactNativeProperties(
|
|
193
|
+
originalWireframe = eq(mockTextWireframe),
|
|
194
|
+
view = eq(mockTextView),
|
|
195
|
+
pixelDensity = eq(0f)
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
// When
|
|
199
|
+
val result =
|
|
200
|
+
testedUtils.mapTextViewToWireframes(
|
|
201
|
+
wireframes = listOf(mockTextWireframe),
|
|
202
|
+
view = mockTextView,
|
|
203
|
+
mappingContext = mockMappingContext
|
|
204
|
+
)[0] as MobileSegment.Wireframe.TextWireframe
|
|
205
|
+
|
|
206
|
+
// Then
|
|
207
|
+
assertThat(result).isEqualTo(mockTextWireframe)
|
|
110
208
|
}
|
|
111
209
|
|
|
112
210
|
// region addReactNativeProperties
|
|
@@ -118,16 +216,15 @@ internal class ReactTextPropertiesResolverTest {
|
|
|
118
216
|
whenever(mockUiImplementation.resolveShadowNode(any())).thenReturn(null)
|
|
119
217
|
|
|
120
218
|
// When
|
|
121
|
-
val result =
|
|
219
|
+
val result = testedUtils.addReactNativeProperties(mockWireframe, mockTextView, 0f)
|
|
122
220
|
|
|
123
221
|
// Then
|
|
124
222
|
assertThat(result).isEqualTo(mockWireframe)
|
|
125
223
|
}
|
|
126
224
|
|
|
127
225
|
@Test
|
|
128
|
-
fun `M add drawable properties W addReactNativeProperties() { has reactBackgroundDrawable }`
|
|
129
|
-
|
|
130
|
-
) {
|
|
226
|
+
fun `M add drawable properties W addReactNativeProperties() { has reactBackgroundDrawable }`
|
|
227
|
+
(forge: Forge) {
|
|
131
228
|
// Given
|
|
132
229
|
val pixelDensity = 0f
|
|
133
230
|
val fakeBorderRadius = forge.aPositiveFloat()
|
|
@@ -150,18 +247,20 @@ internal class ReactTextPropertiesResolverTest {
|
|
|
150
247
|
backgroundColor = formatAsRgba(fakeBorderColor),
|
|
151
248
|
opacity = 0f,
|
|
152
249
|
cornerRadius = fakeBorderRadius.toLong()
|
|
153
|
-
) to
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
250
|
+
) to
|
|
251
|
+
MobileSegment.ShapeBorder(
|
|
252
|
+
color = formatAsRgba(fakeBorderColor),
|
|
253
|
+
width = fakeBorderWidth.toLong()
|
|
254
|
+
)
|
|
157
255
|
)
|
|
158
256
|
|
|
159
257
|
// When
|
|
160
|
-
val result =
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
258
|
+
val result =
|
|
259
|
+
testedUtils.addReactNativeProperties(
|
|
260
|
+
fakeWireframe,
|
|
261
|
+
mockTextView,
|
|
262
|
+
pixelDensity
|
|
263
|
+
)
|
|
165
264
|
|
|
166
265
|
// Then
|
|
167
266
|
assertThat(result.shapeStyle?.cornerRadius).isEqualTo(fakeBorderRadius.toLong())
|
|
@@ -178,8 +277,9 @@ internal class ReactTextPropertiesResolverTest {
|
|
|
178
277
|
whenever(mockTextView.background).thenReturn(null)
|
|
179
278
|
|
|
180
279
|
// When
|
|
181
|
-
val result =
|
|
182
|
-
|
|
280
|
+
val result =
|
|
281
|
+
testedUtils
|
|
282
|
+
.addReactNativeProperties(fakeWireframe, mockTextView, 0f)
|
|
183
283
|
|
|
184
284
|
// Then
|
|
185
285
|
assertThat(result.textStyle.family)
|
|
@@ -194,7 +294,7 @@ internal class ReactTextPropertiesResolverTest {
|
|
|
194
294
|
.thenReturn(null)
|
|
195
295
|
|
|
196
296
|
// When
|
|
197
|
-
val result =
|
|
297
|
+
val result = testedUtils.addReactNativeProperties(fakeWireframe, mockTextView, 0f)
|
|
198
298
|
|
|
199
299
|
// Then
|
|
200
300
|
assertThat(result.textStyle.family).isEqualTo(fakeWireframe.textStyle.family)
|
|
@@ -212,7 +312,7 @@ internal class ReactTextPropertiesResolverTest {
|
|
|
212
312
|
whenever(mockTextAttributes.effectiveFontSize).thenReturn(fakeTextSize)
|
|
213
313
|
|
|
214
314
|
// When
|
|
215
|
-
val result =
|
|
315
|
+
val result = testedUtils.addReactNativeProperties(fakeWireframe, mockTextView, 0f)
|
|
216
316
|
|
|
217
317
|
// Then
|
|
218
318
|
assertThat(result.textStyle.size).isEqualTo(fakeTextSize.toLong())
|
|
@@ -228,7 +328,7 @@ internal class ReactTextPropertiesResolverTest {
|
|
|
228
328
|
.thenReturn(null)
|
|
229
329
|
|
|
230
330
|
// When
|
|
231
|
-
val result =
|
|
331
|
+
val result = testedUtils.addReactNativeProperties(fakeWireframe, mockTextView, 0f)
|
|
232
332
|
|
|
233
333
|
// Then
|
|
234
334
|
assertThat(result.textStyle.size).isEqualTo(fakeWireframe.textStyle.size)
|
|
@@ -246,7 +346,7 @@ internal class ReactTextPropertiesResolverTest {
|
|
|
246
346
|
.thenReturn(fakeTextColor)
|
|
247
347
|
|
|
248
348
|
// When
|
|
249
|
-
val result =
|
|
349
|
+
val result = testedUtils.addReactNativeProperties(fakeWireframe, mockTextView, 0f)
|
|
250
350
|
|
|
251
351
|
// Then
|
|
252
352
|
assertThat(result.textStyle.color).isEqualTo(formatAsRgba(fakeTextColor))
|
|
@@ -264,7 +364,7 @@ internal class ReactTextPropertiesResolverTest {
|
|
|
264
364
|
.thenReturn(fakeTextColor)
|
|
265
365
|
|
|
266
366
|
// When
|
|
267
|
-
val result =
|
|
367
|
+
val result = testedUtils.addReactNativeProperties(fakeWireframe, mockTextView, 0f)
|
|
268
368
|
|
|
269
369
|
// Then
|
|
270
370
|
assertThat(result.textStyle.color).isEqualTo("#000000FF")
|
|
@@ -280,7 +380,7 @@ internal class ReactTextPropertiesResolverTest {
|
|
|
280
380
|
.thenReturn(null)
|
|
281
381
|
|
|
282
382
|
// When
|
|
283
|
-
val result =
|
|
383
|
+
val result = testedUtils.addReactNativeProperties(fakeWireframe, mockTextView, 0f)
|
|
284
384
|
|
|
285
385
|
// Then
|
|
286
386
|
assertThat(result.textStyle.color).isEqualTo(fakeWireframe.textStyle.color)
|
|
@@ -294,11 +394,44 @@ internal class ReactTextPropertiesResolverTest {
|
|
|
294
394
|
.thenReturn(MONOSPACE_FAMILY_NAME)
|
|
295
395
|
|
|
296
396
|
// When
|
|
297
|
-
val result =
|
|
397
|
+
val result = testedUtils.addReactNativeProperties(fakeWireframe, mockTextView, 0f)
|
|
298
398
|
|
|
299
399
|
// Then
|
|
300
400
|
assertThat(result.textStyle.family).isNotEqualTo(MONOSPACE_FAMILY_NAME)
|
|
301
401
|
}
|
|
302
402
|
|
|
403
|
+
@Test
|
|
404
|
+
fun `M return fabric textStyle (color) W addReactNativeProperties`() {
|
|
405
|
+
val mockForegroundColorSpan = mock(ForegroundColorSpan::class.java)
|
|
406
|
+
whenever(mockForegroundColorSpan.foregroundColor).thenReturn(-1)
|
|
407
|
+
|
|
408
|
+
val spannable = mock(Spannable::class.java)
|
|
409
|
+
doReturn(spannable).whenever(fabricTestedUtils).getFieldFromView(any(), any())
|
|
410
|
+
|
|
411
|
+
whenever(spannable.getSpans(anyInt(), anyInt(), eq(ForegroundColorSpan::class.java)))
|
|
412
|
+
.thenReturn(
|
|
413
|
+
arrayOf(mockForegroundColorSpan)
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
val result = fabricTestedUtils.addReactNativeProperties(fakeWireframe, mockTextView, 0f)
|
|
417
|
+
assertThat(result.textStyle.color).isEqualTo("#ffffffff")
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// endregion
|
|
421
|
+
|
|
422
|
+
// region getUiManagerModule
|
|
423
|
+
@Test
|
|
424
|
+
fun `M return null W getUiManagerModule() { cannot get uiManagerModule }`() {
|
|
425
|
+
// Given
|
|
426
|
+
whenever(mockReactContext.getNativeModule(any<Class<NativeModule>>()))
|
|
427
|
+
.thenThrow(IllegalStateException())
|
|
428
|
+
|
|
429
|
+
// When
|
|
430
|
+
val uiManagerModule = testedUtils.getUiManagerModule()
|
|
431
|
+
|
|
432
|
+
// Then
|
|
433
|
+
assertThat(uiManagerModule).isNull()
|
|
434
|
+
}
|
|
435
|
+
|
|
303
436
|
// endregion
|
|
304
437
|
}
|