From e3a9a7659bb111b80186d26a04237a4e75427db1 Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Fri, 6 Dec 2024 17:22:55 +0100 Subject: [PATCH] Android: added ReactImageViewMapper for SR Image Recording --- packages/core/android/build.gradle | 8 +- .../android/build.gradle | 3 +- ...eactNativeSessionReplayExtensionSupport.kt | 19 ++- .../ReactTextPropertiesResolver.kt | 4 +- .../sessionreplay/extensions/LongExt.kt | 15 -- .../extensions/ReactDrawablesExt.kt | 154 ++++++++++++++++++ .../mappers/ReactNativeImageViewMapper.kt | 107 ++++++++++++ .../resources/ReactDrawableCopier.kt | 34 ++++ .../utils/ReactViewBackgroundDrawableUtils.kt | 6 +- ...NativeSessionReplayExtensionSupportTest.kt | 10 +- 10 files changed, 328 insertions(+), 32 deletions(-) delete mode 100644 packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/extensions/LongExt.kt create mode 100644 packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/extensions/ReactDrawablesExt.kt create mode 100644 packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactNativeImageViewMapper.kt create mode 100644 packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/resources/ReactDrawableCopier.kt diff --git a/packages/core/android/build.gradle b/packages/core/android/build.gradle index 3883de8f5..b628047b3 100644 --- a/packages/core/android/build.gradle +++ b/packages/core/android/build.gradle @@ -192,10 +192,10 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compileOnly "com.squareup.okhttp3:okhttp:3.12.13" - implementation "com.datadoghq:dd-sdk-android-rum:2.14.0" - implementation "com.datadoghq:dd-sdk-android-logs:2.14.0" - implementation "com.datadoghq:dd-sdk-android-trace:2.14.0" - implementation "com.datadoghq:dd-sdk-android-webview:2.14.0" + implementation "com.datadoghq:dd-sdk-android-rum:2.16.1" + implementation "com.datadoghq:dd-sdk-android-logs:2.16.1" + implementation "com.datadoghq:dd-sdk-android-trace:2.16.1" + implementation "com.datadoghq:dd-sdk-android-webview:2.16.1" implementation "com.google.code.gson:gson:2.10.0" testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2" diff --git a/packages/react-native-session-replay/android/build.gradle b/packages/react-native-session-replay/android/build.gradle index 2ec53c556..358ac3b25 100644 --- a/packages/react-native-session-replay/android/build.gradle +++ b/packages/react-native-session-replay/android/build.gradle @@ -194,7 +194,8 @@ dependencies { api "com.facebook.react:react-android:$reactNativeVersion" } implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation "com.datadoghq:dd-sdk-android-session-replay:2.14.0" + implementation "com.datadoghq:dd-sdk-android-session-replay:2.16.1" + implementation "com.datadoghq:dd-sdk-android-internal:2.16.1" implementation project(path: ':datadog_mobile-react-native') testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupport.kt b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupport.kt index bce3bd6b8..8212bd990 100644 --- a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupport.kt +++ b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupport.kt @@ -11,11 +11,14 @@ import com.datadog.android.api.InternalLogger import com.datadog.android.sessionreplay.ExtensionSupport import com.datadog.android.sessionreplay.MapperTypeWrapper import com.datadog.android.sessionreplay.recorder.OptionSelectorDetector +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper import com.datadog.reactnative.sessionreplay.mappers.ReactEditTextMapper +import com.datadog.reactnative.sessionreplay.mappers.ReactNativeImageViewMapper import com.datadog.reactnative.sessionreplay.mappers.ReactTextMapper import com.datadog.reactnative.sessionreplay.mappers.ReactViewGroupMapper import com.facebook.react.bridge.ReactContext import com.facebook.react.uimanager.UIManagerModule +import com.facebook.react.views.image.ReactImageView import com.facebook.react.views.text.ReactTextView import com.facebook.react.views.textinput.ReactEditText import com.facebook.react.views.view.ReactViewGroup @@ -24,21 +27,21 @@ internal class ReactNativeSessionReplayExtensionSupport( private val reactContext: ReactContext, private val logger: InternalLogger ) : ExtensionSupport { + override fun name(): String { + return ReactNativeSessionReplayExtensionSupport::class.java.simpleName + } override fun getCustomViewMappers(): List> { val uiManagerModule = getUiManagerModule() return listOf( + MapperTypeWrapper(ReactImageView::class.java, ReactNativeImageViewMapper()), MapperTypeWrapper(ReactViewGroup::class.java, ReactViewGroupMapper()), MapperTypeWrapper(ReactTextView::class.java, ReactTextMapper(reactContext, uiManagerModule)), MapperTypeWrapper(ReactEditText::class.java, ReactEditTextMapper(reactContext, uiManagerModule)), ) } - override fun getOptionSelectorDetectors(): List { - return listOf() - } - @VisibleForTesting internal fun getUiManagerModule(): UIManagerModule? { return try { @@ -54,6 +57,14 @@ internal class ReactNativeSessionReplayExtensionSupport( } } + override fun getOptionSelectorDetectors(): List { + return listOf() + } + + override fun getCustomDrawableMapper(): List { + return emptyList() + } + internal companion object { internal const val RESOLVE_UIMANAGERMODULE_ERROR = "Unable to resolve UIManagerModule" } diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactTextPropertiesResolver.kt b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactTextPropertiesResolver.kt index 91a9d6560..5527c4cd8 100644 --- a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactTextPropertiesResolver.kt +++ b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactTextPropertiesResolver.kt @@ -10,8 +10,8 @@ import ReactViewBackgroundDrawableUtils import android.view.Gravity import android.widget.TextView import androidx.annotation.VisibleForTesting +import com.datadog.android.internal.utils.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.reactnative.sessionreplay.extensions.convertToDensityNormalized import com.datadog.reactnative.sessionreplay.utils.DrawableUtils import com.datadog.reactnative.sessionreplay.utils.ReflectionUtils import com.datadog.reactnative.sessionreplay.utils.formatAsRgba @@ -134,7 +134,7 @@ internal class ReactTextPropertiesResolver( val fontFamily = getFontFamily(shadowNodeWrapper) ?: textWireframe.textStyle.family val fontSize = getFontSize(shadowNodeWrapper) - ?.convertToDensityNormalized(pixelsDensity) + ?.densityNormalized(pixelsDensity) ?: textWireframe.textStyle.size val fontColor = getTextColor(shadowNodeWrapper) ?: textWireframe.textStyle.color diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/extensions/LongExt.kt b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/extensions/LongExt.kt deleted file mode 100644 index 7750479af..000000000 --- a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/extensions/LongExt.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.reactnative.sessionreplay.extensions - -internal fun Long.convertToDensityNormalized(density: Float): Long { - return if (density == 0f) { - this - } else { - (this / density).toLong() - } -} diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/extensions/ReactDrawablesExt.kt b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/extensions/ReactDrawablesExt.kt new file mode 100644 index 000000000..6c6fb5e46 --- /dev/null +++ b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/extensions/ReactDrawablesExt.kt @@ -0,0 +1,154 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.reactnative.sessionreplay.extensions + +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.VectorDrawable +import android.widget.ImageView +import androidx.appcompat.graphics.drawable.DrawerArrowDrawable +import androidx.core.graphics.drawable.toBitmapOrNull +import com.facebook.drawee.drawable.ArrayDrawable +import com.facebook.drawee.drawable.ForwardingDrawable +import com.facebook.drawee.drawable.RoundedBitmapDrawable +import com.facebook.drawee.drawable.ScaleTypeDrawable +import com.facebook.drawee.drawable.ScalingUtils + +internal fun ScaleTypeDrawable.imageViewScaleType(): ImageView.ScaleType? { + return when (this.scaleType) { + ScalingUtils.ScaleType.CENTER -> ImageView.ScaleType.CENTER + ScalingUtils.ScaleType.CENTER_CROP -> ImageView.ScaleType.CENTER_CROP + ScalingUtils.ScaleType.CENTER_INSIDE -> ImageView.ScaleType.CENTER_INSIDE + ScalingUtils.ScaleType.FIT_CENTER -> ImageView.ScaleType.FIT_CENTER + ScalingUtils.ScaleType.FIT_START -> ImageView.ScaleType.FIT_START + ScalingUtils.ScaleType.FIT_END -> ImageView.ScaleType.FIT_END + ScalingUtils.ScaleType.FIT_XY -> ImageView.ScaleType.FIT_XY + else -> null + } +} + +internal fun ArrayDrawable.getScaleTypeDrawable(): ScaleTypeDrawable? { + for (i in 0 until this.numberOfLayers) { + try { + (this.getDrawable(i) as? ScaleTypeDrawable)?.let { + return it + } + } catch(_: IllegalArgumentException) { } + } + + return null +} + +internal fun ArrayDrawable.getDrawableOrNull(index: Int): Drawable? { + return try { + this.getDrawable(index) + } catch (_: IllegalArgumentException) { + null + } +} + +internal fun ForwardingDrawable.tryToExtractBitmap(resources: Resources): Bitmap? { + val forwardedDrawable = this.drawable + return if (forwardedDrawable != null) { + forwardedDrawable.tryToExtractBitmap(resources) + } else { + this.toBitmapOrNull( + this.intrinsicWidth, + this.intrinsicHeight, + Bitmap.Config.ARGB_8888 + ) + } +} + +internal fun RoundedBitmapDrawable.tryToExtractBitmap(): Bitmap? { + val privateBitmap = try { + val field = RoundedBitmapDrawable::class.java.getDeclaredField("mBitmap") + field.isAccessible = true + field.get(this) as? Bitmap + } catch (_: NoSuchFieldException) { + null + } catch (_: IllegalAccessException) { + null + } catch (_: Exception) { + null + } + + return privateBitmap ?: this.toBitmapOrNull( + this.intrinsicWidth, + this.intrinsicHeight, + Bitmap.Config.ARGB_8888 + ) +} + +internal fun BitmapDrawable.tryToExtractBitmap(resources: Resources): Bitmap? { + if (this.bitmap != null) { + return this.bitmap + } + + if (this.constantState != null) { + val copy = this.constantState?.newDrawable(resources) + return (copy as? BitmapDrawable)?.bitmap ?: copy?.toBitmapOrNull( + this.intrinsicWidth, + this.intrinsicHeight, + Bitmap.Config.ARGB_8888 + ) + } + + return null +} + +internal fun ArrayDrawable.tryToExtractBitmap(resources: Resources): Bitmap? { + var width = 0 + var height = 0 + for (index in 0 until this.numberOfLayers) { + val drawable = this.getDrawableOrNull(index) ?: continue + + if (drawable is ScaleTypeDrawable) { + return drawable.tryToExtractBitmap(resources) + } + + if (drawable.intrinsicWidth * drawable.intrinsicHeight > width * height) { + width = drawable.intrinsicWidth + height = drawable.intrinsicHeight + } + } + + return if (width > 0 && height > 0) + this.toBitmapOrNull(width, height, Bitmap.Config.ARGB_8888) + else + null +} + +internal fun Drawable.tryToExtractBitmap( + resources: Resources +): Bitmap? { + when (this) { + is ArrayDrawable -> { + return this.tryToExtractBitmap(resources) + } + is ForwardingDrawable -> { + return this.tryToExtractBitmap(resources) + } + is RoundedBitmapDrawable -> { + return this.tryToExtractBitmap() + } + is BitmapDrawable -> { + return this.tryToExtractBitmap(resources) + } + is VectorDrawable, is ShapeDrawable, is DrawerArrowDrawable -> { + return this.toBitmapOrNull( + this.intrinsicWidth, + this.intrinsicHeight, + Bitmap.Config.ARGB_8888 + ) + } + else -> return null + } +} \ No newline at end of file diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactNativeImageViewMapper.kt b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactNativeImageViewMapper.kt new file mode 100644 index 000000000..eaea51acf --- /dev/null +++ b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactNativeImageViewMapper.kt @@ -0,0 +1,107 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.reactnative.sessionreplay.mappers + +import android.graphics.Rect +import com.datadog.android.api.InternalLogger +import com.datadog.android.internal.utils.ImageViewUtils +import com.datadog.android.internal.utils.densityNormalized +import com.datadog.android.sessionreplay.model.MobileSegment +import com.datadog.android.sessionreplay.recorder.MappingContext +import com.datadog.android.sessionreplay.recorder.mapper.BaseAsyncBackgroundWireframeMapper +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback +import com.datadog.android.sessionreplay.utils.DefaultColorStringFormatter +import com.datadog.android.sessionreplay.utils.DefaultViewBoundsResolver +import com.datadog.android.sessionreplay.utils.DefaultViewIdentifierResolver +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.reactnative.sessionreplay.extensions.getScaleTypeDrawable +import com.datadog.reactnative.sessionreplay.extensions.imageViewScaleType +import com.datadog.reactnative.sessionreplay.resources.ReactDrawableCopier +import com.facebook.drawee.drawable.FadeDrawable +import com.facebook.react.views.image.ReactImageView + +internal class ReactNativeImageViewMapper: BaseAsyncBackgroundWireframeMapper( + viewIdentifierResolver = DefaultViewIdentifierResolver, + colorStringFormatter = DefaultColorStringFormatter, + viewBoundsResolver = DefaultViewBoundsResolver, + drawableToColorMapper = DrawableToColorMapper.getDefault() +) { + private val drawableCopier = ReactDrawableCopier() + + override fun map( + view: ReactImageView, + mappingContext: MappingContext, + asyncJobStatusCallback: AsyncJobStatusCallback, + internalLogger: InternalLogger + ): List { + val wireframes = mutableListOf() + wireframes.addAll(super.map(view, mappingContext, asyncJobStatusCallback, internalLogger)) + + val drawable = view.drawable?.current ?: return wireframes + + val parentRect = ImageViewUtils.resolveParentRectAbsPosition(view) + val scaleType = (drawable as? FadeDrawable) + ?.getScaleTypeDrawable() + ?.imageViewScaleType() ?: view.scaleType + val contentRect = ImageViewUtils.resolveContentRectWithScaling(view, drawable, scaleType) + + val resources = view.resources + val density = resources.displayMetrics.density + + val clipping = if (view.cropToPadding) { + ImageViewUtils.calculateClipping(parentRect, contentRect, density) + } else { + null + } + + val contentXPosInDp = contentRect.left.densityNormalized(density).toLong() + val contentYPosInDp = contentRect.top.densityNormalized(density).toLong() + val contentWidthPx = contentRect.width() + val contentHeightPx = contentRect.height() + + // resolve foreground + mappingContext.imageWireframeHelper.createImageWireframeByDrawable( + view = view, + imagePrivacy = mappingContext.imagePrivacy, + currentWireframeIndex = wireframes.size, + x = contentXPosInDp, + y = contentYPosInDp, + width = contentWidthPx, + height = contentHeightPx, + usePIIPlaceholder = true, + drawable = drawable, + drawableCopier = drawableCopier, + asyncJobStatusCallback = asyncJobStatusCallback, + clipping = clipping?.toWireframeClip(), + shapeStyle = null, + border = null, + prefix = "drawable", + customResourceIdCacheKey = generateUUID(view) + )?.let { + wireframes.add(it) + } + + return wireframes + } + + private fun generateUUID(reactImageView: ReactImageView): String { + val source = reactImageView.imageSource?.source ?: + System.identityHashCode(reactImageView).toString() + val drawableType = reactImageView.drawable.current::class.java.name + return "${drawableType}-${source}" + } + + private fun Rect.toWireframeClip(): MobileSegment.WireframeClip { + return MobileSegment.WireframeClip( + top = top.toLong(), + bottom = bottom.toLong(), + left = left.toLong(), + right = right.toLong() + ) + } +} + diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/resources/ReactDrawableCopier.kt b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/resources/ReactDrawableCopier.kt new file mode 100644 index 000000000..ec95074c4 --- /dev/null +++ b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/resources/ReactDrawableCopier.kt @@ -0,0 +1,34 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.reactnative.sessionreplay.resources + +import android.content.res.Resources +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import com.datadog.android.sessionreplay.recorder.resources.DefaultDrawableCopier +import com.datadog.android.sessionreplay.recorder.resources.DrawableCopier +import com.datadog.reactnative.sessionreplay.extensions.tryToExtractBitmap + +internal class ReactDrawableCopier : DrawableCopier { + private val defaultCopier = DefaultDrawableCopier() + + override fun copy( + originalDrawable: Drawable, + resources: Resources + ): Drawable? { + return if (originalDrawable.constantState != null) { + defaultCopier.copy(originalDrawable, resources) + } else { + originalDrawable.tryToExtractBitmap(resources)?.let { bitmap -> + BitmapDrawable(resources, bitmap).apply { + bounds = originalDrawable.bounds + alpha = originalDrawable.alpha + } + } + } + } +} diff --git a/packages/react-native-session-replay/android/src/rnlegacy/kotlin/com.datadog.reactnative.sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt b/packages/react-native-session-replay/android/src/rnlegacy/kotlin/com.datadog.reactnative.sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt index 0306bb72b..2cad32bff 100644 --- a/packages/react-native-session-replay/android/src/rnlegacy/kotlin/com.datadog.reactnative.sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt +++ b/packages/react-native-session-replay/android/src/rnlegacy/kotlin/com.datadog.reactnative.sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt @@ -1,8 +1,8 @@ import android.graphics.drawable.Drawable import android.graphics.drawable.InsetDrawable import android.graphics.drawable.LayerDrawable +import com.datadog.android.internal.utils.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.reactnative.sessionreplay.extensions.convertToDensityNormalized import com.datadog.reactnative.sessionreplay.utils.DrawableUtils import com.datadog.reactnative.sessionreplay.utils.formatAsRgba import com.facebook.react.uimanager.Spacing @@ -22,7 +22,7 @@ internal class ReactViewBackgroundDrawableUtils() : DrawableUtils() { val cornerRadius = drawable .fullBorderRadius .toLong() - .convertToDensityNormalized(pixelDensity) + .densityNormalized(pixelDensity) val backgroundColor = getBackgroundColor(drawable) val colorHexString = if (backgroundColor != null) { @@ -64,7 +64,7 @@ internal class ReactViewBackgroundDrawableUtils() : DrawableUtils() { pixelDensity: Float ): MobileSegment.ShapeBorder { val borderWidth = - backgroundDrawable.fullBorderWidth.toLong().convertToDensityNormalized(pixelDensity) + backgroundDrawable.fullBorderWidth.toLong().densityNormalized(pixelDensity) val borderColor = formatAsRgba(backgroundDrawable.getBorderColor(Spacing.ALL)) return MobileSegment.ShapeBorder( diff --git a/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupportTest.kt b/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupportTest.kt index de64684de..6ebf23bbf 100644 --- a/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupportTest.kt +++ b/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupportTest.kt @@ -8,6 +8,7 @@ package com.datadog.reactnative.sessionreplay import com.datadog.android.api.InternalLogger import com.datadog.reactnative.sessionreplay.mappers.ReactEditTextMapper +import com.datadog.reactnative.sessionreplay.mappers.ReactNativeImageViewMapper import com.datadog.reactnative.sessionreplay.mappers.ReactTextMapper import com.datadog.reactnative.sessionreplay.mappers.ReactViewGroupMapper import com.facebook.react.bridge.NativeModule @@ -62,15 +63,18 @@ internal class ReactNativeSessionReplayExtensionSupportTest { val customViewMappers = testedExtensionSupport.getCustomViewMappers() // Then - assertThat(customViewMappers).hasSize(3) + assertThat(customViewMappers).hasSize(4) assertThat(customViewMappers[0].getUnsafeMapper()) - .isInstanceOf(ReactViewGroupMapper::class.java) + .isInstanceOf(ReactNativeImageViewMapper::class.java) assertThat(customViewMappers[1].getUnsafeMapper()) - .isInstanceOf(ReactTextMapper::class.java) + .isInstanceOf(ReactViewGroupMapper::class.java) assertThat(customViewMappers[2].getUnsafeMapper()) + .isInstanceOf(ReactTextMapper::class.java) + + assertThat(customViewMappers[3].getUnsafeMapper()) .isInstanceOf(ReactEditTextMapper::class.java) }