Skip to content

Commit 765816a

Browse files
committed
test(android): cover MaskCollector precedence rules
1 parent 7811b4f commit 765816a

1 file changed

Lines changed: 126 additions & 0 deletions

File tree

  • sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/masking
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package com.launchdarkly.observability.replay.masking
2+
3+
import android.view.View
4+
import android.view.ViewGroup
5+
import com.launchdarkly.observability.context.ObserveLogger
6+
import io.mockk.every
7+
import io.mockk.mockk
8+
import org.junit.jupiter.api.Assertions.assertEquals
9+
import org.junit.jupiter.api.Test
10+
11+
/** Behavioral tests for [MaskCollector]'s precedence rules. */
12+
class MaskCollectorTest {
13+
private val logger = mockk<ObserveLogger>(relaxed = true)
14+
private val collector = MaskCollector(logger)
15+
16+
/** Matcher that matches every target. Used to stand in for a "global config matched" signal. */
17+
private val matchAll = object : MaskMatcher {
18+
override fun isMatch(target: MaskTarget): Boolean = true
19+
}
20+
21+
/**
22+
* Builds a mocked leaf [View] with a controllable per-view masking signal. width/height are
23+
* positive so [NativeMaskTarget.mask] returns a non-null Mask, and `isShown=true` so
24+
* traversal visits the view. The masking signal is stubbed onto `view.getTag(any())` because
25+
* `NativeMaskTarget.hasLDMask` / `hasLDUnmask` look it up there via `R.id.ld_mask_tag`.
26+
*
27+
* @param ldMaskTag value returned from `view.getTag(R.id.ld_mask_tag)`. `true` = explicit
28+
* mask (`ldMask()`), `false` = explicit unmask (`ldUnmask()`), `null` = no signal.
29+
*/
30+
private fun mockLeaf(ldMaskTag: Boolean? = null): View = mockk<View>(relaxed = true).also {
31+
every { it.isShown } returns true
32+
every { it.getTag(any()) } returns ldMaskTag
33+
every { it.width } returns 10
34+
every { it.height } returns 10
35+
every { it.id } returns View.NO_ID
36+
}
37+
38+
/**
39+
* Builds a mocked [ViewGroup] containing the given [children], plus the same per-view stubs
40+
* as [mockLeaf].
41+
*
42+
* @param children child views in declaration order; will be returned by `getChildAt(i)`.
43+
* @param ldMaskTag value returned from `view.getTag(R.id.ld_mask_tag)`. See [mockLeaf].
44+
*/
45+
private fun mockGroup(vararg children: View, ldMaskTag: Boolean? = null): ViewGroup =
46+
mockk<ViewGroup>(relaxed = true).also {
47+
every { it.isShown } returns true
48+
every { it.getTag(any()) } returns ldMaskTag
49+
every { it.width } returns 10
50+
every { it.height } returns 10
51+
every { it.id } returns View.NO_ID
52+
every { it.childCount } returns children.size
53+
children.forEachIndexed { i, c -> every { it.getChildAt(i) } returns c }
54+
}
55+
56+
@Test
57+
fun `ancestor ldMask propagates to descendant`() {
58+
val child = mockLeaf()
59+
val parent = mockGroup(child, ldMaskTag = true)
60+
61+
val masks = collector.collectMasks(parent, emptyList(), emptyList())
62+
63+
// Parent emits a mask via its own hasLDMask; child emits one via inherited mask.
64+
assertEquals(2, masks.size)
65+
}
66+
67+
@Test
68+
fun `descendant ldUnmask does not override ancestor ldMask`() {
69+
val child = mockLeaf(ldMaskTag = false)
70+
val parent = mockGroup(child, ldMaskTag = true)
71+
72+
val masks = collector.collectMasks(parent, emptyList(), emptyList())
73+
74+
// Ancestor mask wins; child's ldUnmask tag is ignored when an ancestor is explicitly masked.
75+
assertEquals(2, masks.size)
76+
}
77+
78+
@Test
79+
fun `ancestor ldUnmask overrides global matcher on descendant`() {
80+
val child = mockLeaf()
81+
val parent = mockGroup(child, ldMaskTag = false)
82+
83+
val masks = collector.collectMasks(parent, emptyList(), listOf(matchAll))
84+
85+
// Inherited unmask suppresses the global match on the descendant; the parent itself is
86+
// also not masked because its own ldUnmask vetoes the global matcher.
87+
assertEquals(0, masks.size)
88+
}
89+
90+
@Test
91+
fun `explicit mask matcher wins over ldUnmask on the same view`() {
92+
// Pins the "mask wins over unmask at the same level" rule. A real-world instance is a
93+
// view configured in `maskXMLViewIds` that also carries `ldUnmask()` — the explicit-mask
94+
// signal wins.
95+
val view = mockLeaf(ldMaskTag = false)
96+
97+
val masks = collector.collectMasks(view, listOf(matchAll), emptyList())
98+
99+
assertEquals(1, masks.size)
100+
}
101+
102+
@Test
103+
fun `ldUnmask on one sibling subtree does not affect another`() {
104+
val unmaskedChild = mockLeaf()
105+
val unmaskedSubtree = mockGroup(unmaskedChild, ldMaskTag = false)
106+
val plainChild = mockLeaf()
107+
val plainSubtree = mockGroup(plainChild)
108+
val root = mockGroup(unmaskedSubtree, plainSubtree)
109+
110+
val masks = collector.collectMasks(root, emptyList(), listOf(matchAll))
111+
112+
// root + plainSubtree + plainChild all match the global matcher (3 masks). The
113+
// unmaskedSubtree branch carries an explicit unmask that propagates down to its child,
114+
// suppressing both (0 masks). Total: 3.
115+
assertEquals(3, masks.size)
116+
}
117+
118+
@Test
119+
fun `view without explicit signal falls through to global matcher`() {
120+
val view = mockLeaf()
121+
122+
val masks = collector.collectMasks(view, emptyList(), listOf(matchAll))
123+
124+
assertEquals(1, masks.size)
125+
}
126+
}

0 commit comments

Comments
 (0)