1 /*
<lambda>null2  * Copyright 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.ui.semantics
18 
19 import androidx.compose.foundation.layout.Box
20 import androidx.compose.foundation.layout.Column
21 import androidx.compose.foundation.layout.Row
22 import androidx.compose.foundation.layout.size
23 import androidx.compose.foundation.lazy.LazyListState
24 import androidx.compose.foundation.lazy.LazyRow
25 import androidx.compose.foundation.lazy.rememberLazyListState
26 import androidx.compose.runtime.Composable
27 import androidx.compose.ui.Modifier
28 import androidx.compose.ui.draw.alpha
29 import androidx.compose.ui.node.RootForTest
30 import androidx.compose.ui.platform.LocalView
31 import androidx.compose.ui.platform.testTag
32 import androidx.compose.ui.semantics.SemanticsProperties.TestTag
33 import androidx.compose.ui.test.junit4.ComposeContentTestRule
34 import androidx.compose.ui.test.junit4.createComposeRule
35 import androidx.compose.ui.test.onNodeWithTag
36 import androidx.compose.ui.unit.dp
37 import androidx.test.ext.junit.runners.AndroidJUnit4
38 import androidx.test.filters.MediumTest
39 import com.google.common.truth.Correspondence
40 import com.google.common.truth.Truth.assertThat
41 import kotlin.test.Test
42 import org.junit.Rule
43 import org.junit.runner.RunWith
44 
45 @MediumTest
46 @RunWith(AndroidJUnit4::class)
47 class SemanticsInfoTest {
48 
49     @get:Rule val rule = createComposeRule()
50 
51     lateinit var semanticsOwner: SemanticsOwner
52 
53     @Test
54     fun contentWithNoSemantics() {
55         // Arrange.
56         rule.setTestContent { Box {} }
57         rule.waitForIdle()
58 
59         // Act.
60         val rootSemantics = semanticsOwner.rootInfo
61 
62         // Assert.
63         assertThat(rootSemantics).isNotNull()
64         assertThat(rootSemantics.parentInfo).isNull()
65         assertThat(rootSemantics.childrenInfo.map { it.semanticsConfiguration })
66             .comparingElementsUsing(SemanticsConfigurationComparator)
67             .containsExactly(null)
68 
69         // Assert extension Functions.
70         assertThat(rootSemantics.nearestParentThatHasSemantics()).isNull()
71         assertThat(rootSemantics.findMergingSemanticsParent()).isNull()
72     }
73 
74     @Test
75     fun singleSemanticsModifier() {
76         // Arrange.
77         rule.setTestContent { Box(Modifier.semantics { this.testTag = "testTag" }) }
78         rule.waitForIdle()
79 
80         // Act.
81         val rootSemantics = semanticsOwner.rootInfo
82         val semantics = rule.getSemanticsInfoForTag("testTag")!!
83 
84         // Assert.
85         assertThat(rootSemantics.parentInfo).isNull()
86         assertThat(rootSemantics.childrenInfo).containsExactly(semantics)
87 
88         assertThat(semantics.parentInfo).isEqualTo(rootSemantics)
89         assertThat(semantics.childrenInfo).isEmpty()
90 
91         // Assert extension Functions.
92         assertThat(rootSemantics.nearestParentThatHasSemantics()).isNull()
93         assertThat(rootSemantics.findMergingSemanticsParent()).isNull()
94         assertThat(rootSemantics.childrenInfo.map { it.semanticsConfiguration })
95             .comparingElementsUsing(SemanticsConfigurationComparator)
96             .containsExactly(SemanticsConfiguration().apply { testTag = "testTag" })
97 
98         assertThat(semantics.nearestParentThatHasSemantics()).isEqualTo(rootSemantics)
99         assertThat(semantics.findMergingSemanticsParent()).isNull()
100         assertThat(semantics.childrenInfo).isEmpty()
101     }
102 
103     @Test
104     fun twoSemanticsModifiers() {
105         // Arrange.
106         rule.setTestContent {
107             Box(Modifier.semantics { this.testTag = "item1" })
108             Box(Modifier.semantics { this.testTag = "item2" })
109         }
110         rule.waitForIdle()
111 
112         // Act.
113         val rootSemantics: SemanticsInfo = semanticsOwner.rootInfo
114         val semantics1 = rule.getSemanticsInfoForTag("item1")
115         val semantics2 = rule.getSemanticsInfoForTag("item2")
116 
117         // Assert.
118         assertThat(rootSemantics.parentInfo).isNull()
119         assertThat(rootSemantics.childrenInfo.map { it.semanticsConfiguration }.toList())
120             .comparingElementsUsing(SemanticsConfigurationComparator)
121             .containsExactly(
122                 SemanticsConfiguration().apply { testTag = "item1" },
123                 SemanticsConfiguration().apply { testTag = "item2" }
124             )
125             .inOrder()
126 
127         assertThat(rootSemantics.childrenInfo.map { it.semanticsConfiguration })
128             .comparingElementsUsing(SemanticsConfigurationComparator)
129             .containsExactly(
130                 SemanticsConfiguration().apply { testTag = "item1" },
131                 SemanticsConfiguration().apply { testTag = "item2" }
132             )
133             .inOrder()
134 
135         checkNotNull(semantics1)
136         assertThat(semantics1.parentInfo).isEqualTo(rootSemantics)
137         assertThat(semantics1.childrenInfo).isEmpty()
138 
139         checkNotNull(semantics2)
140         assertThat(semantics2.parentInfo).isEqualTo(rootSemantics)
141         assertThat(semantics2.childrenInfo).isEmpty()
142 
143         // Assert extension Functions.
144         assertThat(rootSemantics.nearestParentThatHasSemantics()).isNull()
145         assertThat(rootSemantics.findMergingSemanticsParent()).isNull()
146         assertThat(rootSemantics.childrenInfo.map { it.semanticsConfiguration })
147             .comparingElementsUsing(SemanticsConfigurationComparator)
148             .containsExactly(
149                 SemanticsConfiguration().apply { testTag = "item1" },
150                 SemanticsConfiguration().apply { testTag = "item2" }
151             )
152             .inOrder()
153 
154         assertThat(semantics1.nearestParentThatHasSemantics()).isEqualTo(rootSemantics)
155         assertThat(semantics1.findMergingSemanticsParent()).isNull()
156         assertThat(semantics1.childrenInfo).isEmpty()
157 
158         assertThat(semantics2.nearestParentThatHasSemantics()).isEqualTo(rootSemantics)
159         assertThat(semantics2.findMergingSemanticsParent()).isNull()
160         assertThat(semantics2.childrenInfo).isEmpty()
161     }
162 
163     // TODO(ralu): Split this into multiple tests.
164     @Test
165     fun nodeDeepInHierarchy() {
166         // Arrange.
167         rule.setTestContent {
168             Column(Modifier.semantics(mergeDescendants = true) { testTag = "outerColumn" }) {
169                 Row(Modifier.semantics { testTag = "outerRow" }) {
170                     Column(Modifier.semantics(mergeDescendants = true) { testTag = "column" }) {
171                         Row(Modifier.semantics { testTag = "row" }) {
172                             Column {
173                                 Box(Modifier.semantics { testTag = "box" })
174                                 Row(
175                                     Modifier.semantics {}
176                                         .semantics { testTag = "testTarget" }
177                                         .semantics { testTag = "extra modifier2" }
178                                 ) {
179                                     Box { Box(Modifier.semantics { testTag = "child1" }) }
180                                     Box(Modifier.semantics { testTag = "child2" }) {
181                                         Box(Modifier.semantics { testTag = "grandChild" })
182                                     }
183                                     Box {}
184                                     Row {
185                                         Box {}
186                                         Box {}
187                                     }
188                                     Box { Box(Modifier.semantics { testTag = "child3" }) }
189                                 }
190                             }
191                         }
192                     }
193                 }
194             }
195         }
196         rule.waitForIdle()
197         val row = rule.getSemanticsInfoForTag(tag = "row", useUnmergedTree = true)
198         val column = rule.getSemanticsInfoForTag("column")
199 
200         // Act.
201         val testTarget = rule.getSemanticsInfoForTag(tag = "testTarget", useUnmergedTree = true)
202 
203         // Assert.
204         checkNotNull(testTarget)
205         assertThat(testTarget.parentInfo).isNotEqualTo(row)
206         assertThat(testTarget.nearestParentThatHasSemantics()).isEqualTo(row)
207         assertThat(testTarget.findMergingSemanticsParent()).isEqualTo(column)
208         assertThat(testTarget.childrenInfo.map { it.semanticsConfiguration })
209             .comparingElementsUsing(SemanticsConfigurationComparator)
210             .containsExactly(
211                 null,
212                 SemanticsConfiguration().apply { testTag = "child2" },
213                 null,
214                 null,
215                 null
216             )
217             .inOrder()
218         assertThat(testTarget.semanticsConfiguration?.getOrNull(TestTag)).isEqualTo("testTarget")
219     }
220 
221     @Test
222     fun readingSemanticsConfigurationOfDeactivatedNode() {
223         // Arrange.
224         lateinit var lazyListState: LazyListState
225         lateinit var rootForTest: RootForTest
226         rule.setContent {
227             rootForTest = LocalView.current as RootForTest
228             lazyListState = rememberLazyListState()
229             LazyRow(state = lazyListState, modifier = Modifier.size(10.dp)) {
230                 items(2) { index -> Box(Modifier.size(10.dp).testTag("$index")) }
231             }
232         }
233         val semanticsId = rule.onNodeWithTag("0").semanticsId()
234         val semanticsInfo = checkNotNull(rootForTest.semanticsOwner[semanticsId])
235 
236         // Act.
237         rule.runOnIdle { lazyListState.requestScrollToItem(1) }
238         val semanticsConfiguration = rule.runOnIdle { semanticsInfo.semanticsConfiguration }
239 
240         // Assert.
241         rule.runOnIdle {
242             assertThat(semanticsInfo.isDeactivated).isTrue()
243             assertThat(semanticsConfiguration).isNull()
244         }
245     }
246 
247     @Test
248     fun transparent() {
249         // Arrange.
250         rule.setTestContent { Box(Modifier.alpha(0.0f)) { Box(Modifier.testTag("item")) } }
251         rule.waitForIdle()
252 
253         // Act.
254         val semantics = rule.getSemanticsInfoForTag("item")
255 
256         // Assert.
257         assertThat(semantics?.isTransparent()).isTrue()
258     }
259 
260     @Test
261     fun semiTransparent() {
262         // Arrange.
263         rule.setTestContent { Box(Modifier.alpha(0.5f)) { Box(Modifier.testTag("item")) } }
264         rule.waitForIdle()
265 
266         // Act.
267         val semantics = rule.getSemanticsInfoForTag("item")
268 
269         // Assert.
270         assertThat(semantics?.isTransparent()).isFalse()
271     }
272 
273     @Test
274     fun nonTransparent() {
275         // Arrange.
276         rule.setTestContent { Box(Modifier.alpha(1.0f)) { Box(Modifier.testTag("item")) } }
277         rule.waitForIdle()
278 
279         // Act.
280         val semantics = rule.getSemanticsInfoForTag("item")
281 
282         // Assert.
283         assertThat(semantics?.isTransparent()).isFalse()
284     }
285 
286     @Test
287     fun transparencyOfStackedItems() {
288         // Arrange.
289         rule.setTestContent {
290             Box(Modifier.alpha(1.0f)) { Box(Modifier.testTag("item1")) }
291             Box(Modifier.alpha(1.0f)) { Box(Modifier.testTag("item2")) }
292             Box(Modifier.alpha(0.0f)) { Box(Modifier.testTag("item3")) }
293         }
294         rule.waitForIdle()
295 
296         // Act.
297         val semantics1 = rule.getSemanticsInfoForTag("item1")
298         val semantics2 = rule.getSemanticsInfoForTag("item2")
299         val semantics3 = rule.getSemanticsInfoForTag("item3")
300 
301         // Assert.
302         assertThat(semantics1?.isTransparent()).isFalse()
303         assertThat(semantics2?.isTransparent()).isFalse()
304         assertThat(semantics3?.isTransparent()).isTrue()
305     }
306 
307     private fun ComposeContentTestRule.setTestContent(composable: @Composable () -> Unit) {
308         setContent {
309             semanticsOwner = (LocalView.current as RootForTest).semanticsOwner
310             composable()
311         }
312     }
313 
314     private fun ComposeContentTestRule.getSemanticsInfoForTag(
315         tag: String,
316         useUnmergedTree: Boolean = true
317     ): SemanticsInfo? {
318         return semanticsOwner[onNodeWithTag(tag, useUnmergedTree).semanticsId()]
319     }
320 
321     companion object {
322         private val SemanticsConfigurationComparator =
323             Correspondence.from<SemanticsConfiguration, SemanticsConfiguration>(
324                 { actual, expected ->
325                     (actual == null && expected == null) ||
326                         (actual != null &&
327                             expected != null &&
328                             actual.getOrNull(TestTag) == expected.getOrNull(TestTag))
329                 },
330                 "has same test tag as "
331             )
332     }
333 }
334