@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.
Files changed (29) hide show
  1. package/README.md +3 -3
  2. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt +5 -5
  3. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupport.kt +5 -29
  4. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ShadowNodeWrapper.kt +2 -2
  5. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactEditTextMapper.kt +3 -26
  6. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactTextMapper.kt +3 -25
  7. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/text/FabricTextViewUtils.kt +74 -0
  8. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/text/LegacyTextViewUtils.kt +118 -0
  9. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/{ReactTextPropertiesResolver.kt → utils/text/TextViewUtils.kt} +60 -96
  10. package/android/src/rn75/kotlin/com/datadog/reactnative/sessionreplay/extensions/LengthPercentageExt.kt +1 -2
  11. package/android/src/rn75/kotlin/com/datadog/reactnative/sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt +1 -3
  12. package/android/src/rn76/kotlin/com/datadog/reactnative/sessionreplay/extensions/LengthPercentageExt.kt +1 -1
  13. package/android/src/rn76/kotlin/com/datadog/reactnative/sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt +1 -3
  14. package/android/src/rnlegacy/kotlin/com.datadog.reactnative.sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt +2 -7
  15. package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupportTest.kt +3 -17
  16. package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/{ReactTextPropertiesResolverTest.kt → utils/text/TextViewUtilsTest.kt} +171 -38
  17. package/ios/Sources/DdSessionReplay.mm +4 -4
  18. package/ios/Sources/DdSessionReplayImplementation.swift +13 -3
  19. package/ios/Sources/RCTFabricWrapper.h +13 -0
  20. package/ios/Sources/RCTFabricWrapper.mm +120 -0
  21. package/ios/Sources/RCTTextPropertiesWrapper.h +23 -0
  22. package/ios/Sources/RCTTextPropertiesWrapper.mm +28 -0
  23. package/ios/Sources/RCTTextViewRecorder.swift +69 -49
  24. package/ios/Sources/RCTVersion.h +8 -0
  25. package/package.json +5 -3
  26. package/scripts/set-ios-rn-version.js +47 -0
  27. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/NoopTextPropertiesResolver.kt +0 -22
  28. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/TextViewUtils.kt +0 -40
  29. 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.internal.utils.densityNormalized
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
- import com.facebook.react.uimanager.UIManagerModule
20
- import com.facebook.react.views.text.TextAttributes
21
- import java.util.Locale
22
-
23
- internal class ReactTextPropertiesResolver(
24
- private val reactContext: ReactContext,
25
- private val uiManagerModule: UIManagerModule,
26
- private val reflectionUtils: ReflectionUtils = ReflectionUtils(),
27
- private val drawableUtils: DrawableUtils =
28
- ReactViewBackgroundDrawableUtils()
29
- ): TextPropertiesResolver {
30
- override fun addReactNativeProperties(
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
- private fun resolveTextStyleAndPosition(
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 shadowNodeWrapper: ShadowNodeWrapper =
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
- private fun resolveShapeStyleAndBorder(
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
- private fun resolveTextAlignment(
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
- private fun resolveTextStyle(
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
- internal companion object {
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
+ }
@@ -9,5 +9,4 @@ import com.facebook.react.uimanager.LengthPercentage
9
9
 
10
10
  internal fun LengthPercentage?.getRadius(width: Float, height: Float) = this
11
11
  ?.resolve(width, height)
12
- ?.let { (it.horizontal + it.vertical) / 2f }
13
- ?: 0f
12
+ ?: 0f
@@ -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(
@@ -10,4 +10,4 @@ import com.facebook.react.uimanager.LengthPercentage
10
10
  internal fun LengthPercentage?.getRadius(width: Float, height: Float) = this
11
11
  ?.resolve(width, height)
12
12
  ?.let { (it.horizontal + it.vertical) / 2f }
13
- ?: 0f
13
+ ?: 0f
@@ -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
- testedExtensionSupport = ReactNativeSessionReplayExtensionSupport(
55
- logger = mockLogger,
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.reactnative.sessionreplay.ReactTextPropertiesResolver.Companion.COLOR_FIELD_NAME
12
- import com.datadog.reactnative.sessionreplay.ReactTextPropertiesResolver.Companion.FONT_FAMILY_FIELD_NAME
13
- import com.datadog.reactnative.sessionreplay.ReactTextPropertiesResolver.Companion.IS_COLOR_SET_FIELD_NAME
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 ReactTextPropertiesResolverTest {
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(mockUiManagerModule, UI_IMPLEMENTATION_FIELD_NAME)
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
- testedResolver = ReactTextPropertiesResolver(
105
- reactContext = mockReactContext,
106
- uiManagerModule = mockUiManagerModule,
107
- drawableUtils = mockDrawableUtils,
108
- reflectionUtils = mockReflectionUtils
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 = testedResolver.addReactNativeProperties(mockWireframe, mockTextView, 0f)
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
- forge: Forge
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 MobileSegment.ShapeBorder(
154
- color = formatAsRgba(fakeBorderColor),
155
- width = fakeBorderWidth.toLong()
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 = testedResolver.addReactNativeProperties(
161
- fakeWireframe,
162
- mockTextView,
163
- pixelDensity
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 = testedResolver
182
- .addReactNativeProperties(fakeWireframe, mockTextView, 0f)
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 = testedResolver.addReactNativeProperties(fakeWireframe, mockTextView, 0f)
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 = testedResolver.addReactNativeProperties(fakeWireframe, mockTextView, 0f)
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 = testedResolver.addReactNativeProperties(fakeWireframe, mockTextView, 0f)
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 = testedResolver.addReactNativeProperties(fakeWireframe, mockTextView, 0f)
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 = testedResolver.addReactNativeProperties(fakeWireframe, mockTextView, 0f)
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 = testedResolver.addReactNativeProperties(fakeWireframe, mockTextView, 0f)
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 = testedResolver.addReactNativeProperties(fakeWireframe, mockTextView, 0f)
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
  }