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