Skip to content

Commit

Permalink
Merge pull request #1684 from DataDog/jmoskovich/rum-1613/traversal-m…
Browse files Browse the repository at this point in the history
…apper

RUM-1613 Add mapper interface for traversing all children
  • Loading branch information
jonathanmos authored Nov 7, 2023
2 parents fd0cae0 + 8147727 commit 4db01f1
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 30 deletions.
1 change: 1 addition & 0 deletions features/dd-sdk-android-session-replay/api/apiSurface
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class com.datadog.android.sessionreplay.internal.recorder.mapper.MaskTextViewMap
open class com.datadog.android.sessionreplay.internal.recorder.mapper.TextViewMapper : BaseAsyncBackgroundWireframeMapper<android.widget.TextView>
constructor()
override fun map(android.widget.TextView, com.datadog.android.sessionreplay.internal.recorder.MappingContext, com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback): List<com.datadog.android.sessionreplay.model.MobileSegment.Wireframe>
interface com.datadog.android.sessionreplay.internal.recorder.mapper.TraverseAllChildrenMapper<T: android.view.View, S: com.datadog.android.sessionreplay.model.MobileSegment.Wireframe> : WireframeMapper<T, S>
interface com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper<T: android.view.View, S: com.datadog.android.sessionreplay.model.MobileSegment.Wireframe>
fun map(T, com.datadog.android.sessionreplay.internal.recorder.MappingContext, com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback = NoOpAsyncJobStatusCallback()): List<S>
object com.datadog.android.sessionreplay.utils.StringUtils
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ public class com/datadog/android/sessionreplay/internal/recorder/mapper/TextView
public fun map (Landroid/widget/TextView;Lcom/datadog/android/sessionreplay/internal/recorder/MappingContext;Lcom/datadog/android/sessionreplay/internal/AsyncJobStatusCallback;)Ljava/util/List;
}

public abstract interface class com/datadog/android/sessionreplay/internal/recorder/mapper/TraverseAllChildrenMapper : com/datadog/android/sessionreplay/internal/recorder/mapper/WireframeMapper {
}

public abstract interface class com/datadog/android/sessionreplay/internal/recorder/mapper/WireframeMapper {
public abstract fun map (Landroid/view/View;Lcom/datadog/android/sessionreplay/internal/recorder/MappingContext;Lcom/datadog/android/sessionreplay/internal/AsyncJobStatusCallback;)Ljava/util/List;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,17 @@ internal class SnapshotProducer(
val traversedTreeView = treeViewTraversal.traverse(view, mappingContext, recordedDataQueueRefs)
val nextTraversalStrategy = traversedTreeView.nextActionStrategy
val resolvedWireframes = traversedTreeView.mappedWireframes
if (nextTraversalStrategy == TreeViewTraversal.TraversalStrategy.STOP_AND_DROP_NODE) {
if (nextTraversalStrategy == TraversalStrategy.STOP_AND_DROP_NODE) {
return null
}
if (nextTraversalStrategy == TreeViewTraversal.TraversalStrategy.STOP_AND_RETURN_NODE) {
if (nextTraversalStrategy == TraversalStrategy.STOP_AND_RETURN_NODE) {
return Node(wireframes = resolvedWireframes, parents = parents)
}

val childNodes = LinkedList<Node>()
if (view is ViewGroup &&
view.childCount > 0 &&
nextTraversalStrategy == TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN
nextTraversalStrategy == TraversalStrategy.TRAVERSE_ALL_CHILDREN
) {
val childMappingContext = resolveChildMappingContext(view, mappingContext)
val parentsCopy = LinkedList(parents).apply { addAll(resolvedWireframes) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueRefs
import com.datadog.android.sessionreplay.internal.recorder.mapper.DecorViewMapper
import com.datadog.android.sessionreplay.internal.recorder.mapper.MapperTypeWrapper
import com.datadog.android.sessionreplay.internal.recorder.mapper.QueueableViewMapper
import com.datadog.android.sessionreplay.internal.recorder.mapper.TraverseAllChildrenMapper
import com.datadog.android.sessionreplay.internal.recorder.mapper.ViewWireframeMapper
import com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper
import com.datadog.android.sessionreplay.model.MobileSegment
Expand Down Expand Up @@ -38,11 +39,16 @@ internal class TreeViewTraversal(
val resolvedWireframes: List<MobileSegment.Wireframe>

// try to resolve from the exhaustive type mappers
val exhaustiveTypeMapper = mappers.findFirstForType(view::class.java)
val mapper = mappers.findFirstForType(view::class.java)

if (exhaustiveTypeMapper != null) {
val queueableViewMapper = QueueableViewMapper(exhaustiveTypeMapper, recordedDataQueueRefs)
traversalStrategy = TraversalStrategy.STOP_AND_RETURN_NODE
if (mapper != null) {
val queueableViewMapper =
QueueableViewMapper(mapper, recordedDataQueueRefs)
traversalStrategy = if (mapper is TraverseAllChildrenMapper) {
TraversalStrategy.TRAVERSE_ALL_CHILDREN
} else {
TraversalStrategy.STOP_AND_RETURN_NODE
}
resolvedWireframes = queueableViewMapper.map(view, mappingContext)
} else if (isDecorView(view)) {
traversalStrategy = TraversalStrategy.TRAVERSE_ALL_CHILDREN
Expand Down Expand Up @@ -73,10 +79,10 @@ internal class TreeViewTraversal(
val mappedWireframes: List<MobileSegment.Wireframe>,
val nextActionStrategy: TraversalStrategy
)
}

enum class TraversalStrategy {
TRAVERSE_ALL_CHILDREN,
STOP_AND_RETURN_NODE,
STOP_AND_DROP_NODE
}
internal enum class TraversalStrategy {
TRAVERSE_ALL_CHILDREN,
STOP_AND_RETURN_NODE,
STOP_AND_DROP_NODE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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.internal.recorder.mapper

import android.view.View
import com.datadog.android.sessionreplay.model.MobileSegment

/**
* Maps a View to a [List] of [MobileSegment.Wireframe].
* This is mainly used internally by the SDK but if you want to provide a different
* Session Replay representation for a specific View type you can implement this on your end.
* Note that mappers using this interface also traverse all the children of the view
* instead of just the immediate one. This means that you will need to have mappers
* for all child views of the view the mapper is traversing.
*/
interface TraverseAllChildrenMapper<in T : View, out S : MobileSegment.Wireframe> :
WireframeMapper<T, S>
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ internal class SnapshotProducerTest {
val mockRoot: View = mock()
val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView(
fakeViewWireframes,
TreeViewTraversal.TraversalStrategy.STOP_AND_DROP_NODE
TraversalStrategy.STOP_AND_DROP_NODE
)
whenever(mockTreeViewTraversal.traverse(eq(mockRoot), any(), any()))
.thenReturn(fakeTraversedTreeView)
Expand All @@ -93,7 +93,7 @@ internal class SnapshotProducerTest {
val fakeRoot = forge.aMockView<View>()
val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView(
fakeViewWireframes,
TreeViewTraversal.TraversalStrategy.STOP_AND_RETURN_NODE
TraversalStrategy.STOP_AND_RETURN_NODE
)
whenever(mockTreeViewTraversal.traverse(eq(fakeRoot), any(), any()))
.thenReturn(fakeTraversedTreeView)
Expand All @@ -118,7 +118,7 @@ internal class SnapshotProducerTest {
val fakeRoot = forge.aMockViewWithChildren(2, 0, 2)
val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView(
fakeViewWireframes,
TreeViewTraversal.TraversalStrategy.STOP_AND_RETURN_NODE
TraversalStrategy.STOP_AND_RETURN_NODE
)
whenever(mockTreeViewTraversal.traverse(any(), any(), any()))
.thenReturn(fakeTraversedTreeView)
Expand All @@ -143,7 +143,7 @@ internal class SnapshotProducerTest {
val fakeRoot = forge.aMockViewWithChildren(2, 0, 2)
val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView(
fakeViewWireframes,
TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN
TraversalStrategy.TRAVERSE_ALL_CHILDREN
)
whenever(mockTreeViewTraversal.traverse(any(), any(), any()))
.thenReturn(fakeTraversedTreeView)
Expand Down Expand Up @@ -172,7 +172,7 @@ internal class SnapshotProducerTest {
}
val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView(
fakeViewWireframes,
TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN
TraversalStrategy.TRAVERSE_ALL_CHILDREN
)
whenever(mockTreeViewTraversal.traverse(any(), any(), any())).thenReturn(fakeTraversedTreeView)

Expand Down Expand Up @@ -204,7 +204,7 @@ internal class SnapshotProducerTest {
}
val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView(
fakeViewWireframes,
TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN
TraversalStrategy.TRAVERSE_ALL_CHILDREN
)
whenever(mockTreeViewTraversal.traverse(any(), any(), any())).thenReturn(fakeTraversedTreeView)
whenever(mockOptionSelectorDetector.isOptionSelector(mockRoot)).thenReturn(true)
Expand Down Expand Up @@ -237,7 +237,7 @@ internal class SnapshotProducerTest {
}
val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView(
fakeViewWireframes,
TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN
TraversalStrategy.TRAVERSE_ALL_CHILDREN
)
whenever(mockTreeViewTraversal.traverse(any(), any(), any())).thenReturn(fakeTraversedTreeView)
whenever(mockOptionSelectorDetector.isOptionSelector(mockRoot)).thenReturn(false)
Expand Down Expand Up @@ -267,14 +267,14 @@ internal class SnapshotProducerTest {
val fakeRoot = forge.aMockViewWithChildren(2, 0, 2)
val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView(
fakeViewWireframes,
TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN
TraversalStrategy.TRAVERSE_ALL_CHILDREN
)
whenever(mockTreeViewTraversal.traverse(any(), any(), any()))
.thenReturn(fakeTraversedTreeView)
.thenReturn(
fakeTraversedTreeView.copy(
nextActionStrategy =
TreeViewTraversal.TraversalStrategy.STOP_AND_RETURN_NODE
TraversalStrategy.STOP_AND_RETURN_NODE
)
)
var expectedSnapshot = fakeRoot.toNode(viewMappedWireframes = fakeViewWireframes)
Expand Down Expand Up @@ -303,14 +303,14 @@ internal class SnapshotProducerTest {
val fakeRoot = forge.aMockViewWithChildren(2, 0, 2)
val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView(
fakeViewWireframes,
TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN
TraversalStrategy.TRAVERSE_ALL_CHILDREN
)
whenever(mockTreeViewTraversal.traverse(any(), any(), any()))
.thenReturn(fakeTraversedTreeView)
.thenReturn(
fakeTraversedTreeView.copy(
nextActionStrategy =
TreeViewTraversal.TraversalStrategy.STOP_AND_DROP_NODE
TraversalStrategy.STOP_AND_DROP_NODE
)
)
val expectedSnapshot = fakeRoot.toNode(viewMappedWireframes = fakeViewWireframes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.datadog.android.sessionreplay.forge.ForgeConfigurator
import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueRefs
import com.datadog.android.sessionreplay.internal.recorder.mapper.DecorViewMapper
import com.datadog.android.sessionreplay.internal.recorder.mapper.MapperTypeWrapper
import com.datadog.android.sessionreplay.internal.recorder.mapper.TraverseAllChildrenMapper
import com.datadog.android.sessionreplay.internal.recorder.mapper.ViewWireframeMapper
import com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper
import com.datadog.android.sessionreplay.model.MobileSegment
Expand Down Expand Up @@ -90,7 +91,9 @@ internal class TreeViewTraversalTest {
)
val fakeTypes: List<Class<*>> = mockViews.map { it::class.java }
val fakeTypeToMapperMap: Map<Class<*>, WireframeMapper<View, *>> = fakeTypes
.associateWith { mock() }
.associateWith {
mock()
}
val fakeTypeMapperWrappers = fakeTypes.map {
val mapper = fakeTypeToMapperMap[it]!!
MapperTypeWrapper(it, mapper)
Expand Down Expand Up @@ -121,7 +124,7 @@ internal class TreeViewTraversalTest {
// Then
assertThat(traversedTreeView.mappedWireframes).isEqualTo(fakeViewMappedWireframes)
assertThat(traversedTreeView.nextActionStrategy)
.isEqualTo(TreeViewTraversal.TraversalStrategy.STOP_AND_RETURN_NODE)
.isEqualTo(TraversalStrategy.STOP_AND_RETURN_NODE)
}

@Test
Expand Down Expand Up @@ -161,7 +164,58 @@ internal class TreeViewTraversalTest {
// Then
assertThat(traversedTreeView.mappedWireframes).isEqualTo(fakeViewMappedWireframes)
assertThat(traversedTreeView.nextActionStrategy)
.isEqualTo(TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN)
.isEqualTo(TraversalStrategy.TRAVERSE_ALL_CHILDREN)
}

@Test
fun `M use TRAVERSE_ALL_CHILDREN traversal strategy W traverse { TraverseAllChildrenMapper }`(
forge: Forge
) {
// Given
val fakeViewMappedWireframes: List<MobileSegment.Wireframe> = forge.aList { getForgery() }
val mockViews: List<View> = listOf(
forge.aMockView<RadioButton>(),
forge.aMockView<CompoundButton>(),
forge.aMockView<CheckedTextView>(),
forge.aMockView<Button>(),
forge.aMockView<TextView>()
)
val fakeTypes: List<Class<*>> = mockViews.map { it::class.java }
val fakeTypeToMapperMap: Map<Class<*>, TraverseAllChildrenMapper<View, *>> = fakeTypes
.associateWith {
mock()
}
val fakeTypeMapperWrappers = fakeTypes.map {
val mapper = fakeTypeToMapperMap[it]!!
MapperTypeWrapper(it, mapper)
}
val mockView = forge.anElementFrom(mockViews)
whenever(
fakeTypeToMapperMap[mockView::class.java]!!.map(
eq(mockView),
eq(fakeMappingContext),
any()
)
)
.thenReturn(fakeViewMappedWireframes)
testedTreeViewTraversal = TreeViewTraversal(
fakeTypeMapperWrappers,
mockViewMapper,
mockDecorViewMapper,
mockViewUtilsInternal
)

// When
val traversedTreeView = testedTreeViewTraversal.traverse(
mockView,
fakeMappingContext,
mockRecordedDataQueueRefs
)

// Then
assertThat(traversedTreeView.mappedWireframes).isEqualTo(fakeViewMappedWireframes)
assertThat(traversedTreeView.nextActionStrategy)
.isEqualTo(TraversalStrategy.TRAVERSE_ALL_CHILDREN)
}

@Test
Expand Down Expand Up @@ -193,7 +247,7 @@ internal class TreeViewTraversalTest {
// Then
assertThat(traversedTreeView.mappedWireframes).isEqualTo(fakeViewMappedWireframes)
assertThat(traversedTreeView.nextActionStrategy)
.isEqualTo(TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN)
.isEqualTo(TraversalStrategy.TRAVERSE_ALL_CHILDREN)
}

@Test
Expand Down Expand Up @@ -226,7 +280,7 @@ internal class TreeViewTraversalTest {
// Then
assertThat(traversedTreeView.mappedWireframes).isEqualTo(fakeViewMappedWireframes)
assertThat(traversedTreeView.nextActionStrategy)
.isEqualTo(TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN)
.isEqualTo(TraversalStrategy.TRAVERSE_ALL_CHILDREN)
}

// endregion
Expand All @@ -250,7 +304,7 @@ internal class TreeViewTraversalTest {
// Then
assertThat(traversedTreeView.mappedWireframes).isEmpty()
assertThat(traversedTreeView.nextActionStrategy)
.isEqualTo(TreeViewTraversal.TraversalStrategy.STOP_AND_DROP_NODE)
.isEqualTo(TraversalStrategy.STOP_AND_DROP_NODE)
}

// endregion
Expand All @@ -274,7 +328,7 @@ internal class TreeViewTraversalTest {
// Then
assertThat(traversedTreeView.mappedWireframes).isEmpty()
assertThat(traversedTreeView.nextActionStrategy)
.isEqualTo(TreeViewTraversal.TraversalStrategy.STOP_AND_DROP_NODE)
.isEqualTo(TraversalStrategy.STOP_AND_DROP_NODE)
}

// endregion
Expand Down

0 comments on commit 4db01f1

Please sign in to comment.