Skip to content

Commit

Permalink
RUM-4738: Add "Tab" and "TabRow" Composable groups mappers
Browse files Browse the repository at this point in the history
  • Loading branch information
ambushwork committed Aug 8, 2024
1 parent 8d4b730 commit c80b414
Show file tree
Hide file tree
Showing 7 changed files with 434 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ internal abstract class AbstractCompositionGroupMapper(
)
}

protected abstract fun map(
abstract fun map(
stableGroupId: Long,
parameters: Sequence<ComposableParameter>,
boxWithDensity: Box,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,9 @@ internal class ComposeWireframeMapper(

private val composeMappers = mapOf<String, CompositionGroupMapper>(
"Text" to TextCompositionGroupMapper(colorStringFormatter),
"Button" to ButtonCompositionGroupMapper(colorStringFormatter)
// TODO RUM-4738 Implement mappers for different Composable groups
// "TabRow" : holds the tab row bg color
// "Tab": holds selected tab info
"Button" to ButtonCompositionGroupMapper(colorStringFormatter),
"Tab" to TabCompositionGroupMapper(colorStringFormatter),
"TabRow" to TabRowCompositionGroupMapper(colorStringFormatter)
)

// region WireframeMapper
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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.android.sessionreplay.compose.internal.mappers

import com.datadog.android.sessionreplay.compose.internal.data.Box
import com.datadog.android.sessionreplay.compose.internal.data.ComposableParameter
import com.datadog.android.sessionreplay.compose.internal.data.ComposeWireframe
import com.datadog.android.sessionreplay.compose.internal.data.UiContext
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.android.sessionreplay.utils.ColorStringFormatter

internal class TabCompositionGroupMapper(
colorStringFormatter: ColorStringFormatter
) : AbstractCompositionGroupMapper(colorStringFormatter) {
override fun map(
stableGroupId: Long,
parameters: Sequence<ComposableParameter>,
boxWithDensity: Box,
uiContext: UiContext
): ComposeWireframe {
val isSelected =
parameters.firstOrNull { it.name == "selected" }?.value as? Boolean ?: false
val textColor = if (isSelected) {
parseSelectedContentColor(parameters)
} else {
parseUnselectedContentColor(parameters)
} ?: uiContext.parentContentColor
return ComposeWireframe(
MobileSegment.Wireframe.ShapeWireframe(
id = stableGroupId,
x = boxWithDensity.x,
y = boxWithDensity.y,
width = boxWithDensity.width,
height = boxWithDensity.height
),
uiContext.copy(parentContentColor = textColor)
)
}

private fun parseSelectedContentColor(params: Sequence<ComposableParameter>): String? {
return (params.firstOrNull { it.name == "selectedContentColor" }?.value as? Long)?.let {
convertColor(it)
}
}

private fun parseUnselectedContentColor(params: Sequence<ComposableParameter>): String? {
return (params.firstOrNull { it.name == "unselectedContentColor" }?.value as? Long)?.let {
convertColor(it)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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.android.sessionreplay.compose.internal.mappers

import com.datadog.android.sessionreplay.compose.internal.data.Box
import com.datadog.android.sessionreplay.compose.internal.data.ComposableParameter
import com.datadog.android.sessionreplay.compose.internal.data.ComposeWireframe
import com.datadog.android.sessionreplay.compose.internal.data.UiContext
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.android.sessionreplay.utils.ColorStringFormatter

internal class TabRowCompositionGroupMapper(
colorStringFormatter: ColorStringFormatter
) : AbstractCompositionGroupMapper(colorStringFormatter) {
override fun map(
stableGroupId: Long,
parameters: Sequence<ComposableParameter>,
boxWithDensity: Box,
uiContext: UiContext
): ComposeWireframe {
var backgroundColor: String? = null
var contentColor: String? = null
parameters.forEach { param ->
when (param.name) {
"backgroundColor" -> (param.value as? Long)?.let {
backgroundColor = convertColor(it)
}

"contentColor" -> (param.value as? Long)?.let {
contentColor = convertColor(it)
}
}
}
return ComposeWireframe(
MobileSegment.Wireframe.ShapeWireframe(
id = stableGroupId,
x = boxWithDensity.x,
y = boxWithDensity.y,
width = boxWithDensity.width,
height = boxWithDensity.height,
shapeStyle = MobileSegment.ShapeStyle(
backgroundColor = backgroundColor
)
),
uiContext.copy(parentContentColor = contentColor)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ internal open class AbstractCompositionGroupMapperTest {
private lateinit var fakeWireframe: ComposeWireframe

@LongForgery
private var fakeGroupId: Long = 0L
var fakeGroupId: Long = 0L

@Mock
lateinit var mockColorStringFormatter: ColorStringFormatter
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
* 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.android.sessionreplay.compose.internal.mappers

import androidx.compose.runtime.tooling.CompositionGroup
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import com.datadog.android.sessionreplay.compose.internal.data.Box
import com.datadog.android.sessionreplay.compose.internal.data.ComposableParameter
import com.datadog.android.sessionreplay.compose.internal.stableId
import com.datadog.android.sessionreplay.compose.test.elmyr.SessionReplayComposeForgeConfigurator
import com.datadog.android.sessionreplay.model.MobileSegment
import fr.xgouchet.elmyr.Forge
import fr.xgouchet.elmyr.junit5.ForgeConfiguration
import fr.xgouchet.elmyr.junit5.ForgeExtension
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.RepeatedTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.extension.Extensions
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.junit.jupiter.MockitoSettings
import org.mockito.kotlin.whenever
import org.mockito.quality.Strictness
import kotlin.math.roundToInt

@Extensions(
ExtendWith(MockitoExtension::class),
ExtendWith(ForgeExtension::class)
)
@MockitoSettings(strictness = Strictness.LENIENT)
@ForgeConfiguration(SessionReplayComposeForgeConfigurator::class)
internal class TabCompositionGroupMapperTest : AbstractCompositionGroupMapperTest() {

private lateinit var tabCompositionGroupMapper: TabCompositionGroupMapper

private lateinit var mockCompositionGroup: CompositionGroup

private lateinit var fakeBoxWithDensity: Box

@BeforeEach
fun `set up`(forge: Forge) {
tabCompositionGroupMapper = TabCompositionGroupMapper(colorStringFormatter = mockColorStringFormatter)
mockCompositionGroup = mockGroupWithCoordinates(forge)
fakeBoxWithDensity = Box(
left = forge.aLong(),
top = forge.aLong(),
right = forge.aLong(),
bottom = forge.aLong()
)
}

@Test
fun `M return the correct context color W tab is selected()`(forge: Forge) {
// Given
val selectedColor = forge.aLong()
val unSelectedColor = forge.aLong()
val selectedColorHexStr = forge.aString(size = 6)
val unSelectedColorHexStr = forge.aString(size = 6)
whenever(
mockColorStringFormatter.formatColorAndAlphaAsHexString(
convertColorIntAlpha(selectedColor).first,
convertColorIntAlpha(selectedColor).second
)
).thenReturn(selectedColorHexStr)
whenever(
mockColorStringFormatter.formatColorAndAlphaAsHexString(
convertColorIntAlpha(unSelectedColor).first,
convertColorIntAlpha(unSelectedColor).second
)
).thenReturn(unSelectedColorHexStr)

// When
val actual = tabCompositionGroupMapper.map(
stableGroupId = fakeGroupId,
parameters = listOf(
ComposableParameter(
name = "selected",
value = true
),
ComposableParameter(
name = "selectedContentColor",
value = selectedColor
),
ComposableParameter(
name = "unselectedContentColor",
value = unSelectedColor
)
).asSequence(),
boxWithDensity = fakeBoxWithDensity,
uiContext = fakeUiContext
)

// Then
Assertions.assertThat(actual.uiContext)
.isEqualTo(fakeUiContext.copy(parentContentColor = selectedColorHexStr))
}

@Test
fun `M return the correct context color W tab is unselected()`(forge: Forge) {
// Given
val selectedColor = forge.aLong()
val unSelectedColor = forge.aLong()
val selectedColorHexStr = forge.aString(size = 6)
val unSelectedColorHexStr = forge.aString(size = 6)
whenever(
mockColorStringFormatter.formatColorAndAlphaAsHexString(
convertColorIntAlpha(selectedColor).first,
convertColorIntAlpha(selectedColor).second
)
).thenReturn(selectedColorHexStr)
whenever(
mockColorStringFormatter.formatColorAndAlphaAsHexString(
convertColorIntAlpha(unSelectedColor).first,
convertColorIntAlpha(unSelectedColor).second
)
).thenReturn(unSelectedColorHexStr)

// When
val actual = tabCompositionGroupMapper.map(
stableGroupId = fakeGroupId,
parameters = listOf(
ComposableParameter(
name = "selected",
value = false
),
ComposableParameter(
name = "selectedContentColor",
value = selectedColor
),
ComposableParameter(
name = "unselectedContentColor",
value = unSelectedColor
)
).asSequence(),
boxWithDensity = fakeBoxWithDensity,
uiContext = fakeUiContext
)

// Then
Assertions.assertThat(actual.uiContext)
.isEqualTo(fakeUiContext.copy(parentContentColor = unSelectedColorHexStr))
}

@RepeatedTest(8)
fun `M return the correct wireframe W map`() {
// Given
val actual = tabCompositionGroupMapper.map(
compositionGroup = mockCompositionGroup,
composeContext = fakeComposeContext,
uiContext = fakeUiContext
)

// When
val expectedBox = requireNotNull(Box.from(compositionGroup = mockCompositionGroup))
val boxWithDensity = expectedBox.withDensity(fakeUiContext.density)
val expected = MobileSegment.Wireframe.ShapeWireframe(
id = mockCompositionGroup.stableId(),
x = boxWithDensity.x,
y = boxWithDensity.y,
width = boxWithDensity.width,
height = boxWithDensity.height
)

// Then
Assertions.assertThat(actual?.wireframe).isEqualTo(expected)
}

private fun convertColorIntAlpha(color: Long): Pair<Int, Int> {
val c = Color(color shr COMPOSE_COLOR_SHIFT)
return Pair(c.toArgb(), (c.alpha * MAX_ALPHA).roundToInt())
}

companion object {
/** As defined in Compose's ColorSpaces. */
private const val COMPOSE_COLOR_SHIFT = 32
private const val MAX_ALPHA = 255
}
}
Loading

0 comments on commit c80b414

Please sign in to comment.