[go: nahoru, domu]

blob: f37970683e19ad43571740473db55d954d8ad585 [file] [log] [blame]
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.ui.semantics
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.test.filters.MediumTest
import androidx.ui.core.Layout
import androidx.ui.core.Modifier
import androidx.ui.core.semantics.semantics
import androidx.ui.core.testTag
import androidx.ui.test.SemanticsNodeInteraction
import androidx.ui.test.SemanticsMatcher
import androidx.ui.test.assertCountEquals
import androidx.ui.test.assertLabelEquals
import androidx.ui.test.assertValueEquals
import androidx.ui.test.createComposeRule
import androidx.ui.test.onAllNodesWithText
import androidx.ui.test.onNodeWithTag
import androidx.ui.test.assert
import androidx.ui.test.onAllNodesWithLabel
import androidx.ui.test.onNodeWithLabel
import androidx.ui.test.runOnIdle
import androidx.ui.test.runOnUiThread
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import java.util.concurrent.CountDownLatch
import kotlin.math.max
@MediumTest
@RunWith(JUnit4::class)
class SemanticsTests {
private val TestTag = "semantics-test-tag"
@get:Rule
val composeTestRule = createComposeRule(disableTransitions = true)
private fun executeUpdateBlocking(updateFunction: () -> Unit) {
val latch = CountDownLatch(1)
runOnUiThread {
updateFunction()
latch.countDown()
}
latch.await()
}
@Test
fun nestedMergedSubtree() {
val tag1 = "tag1"
val tag2 = "tag2"
val label1 = "foo"
val label2 = "bar"
composeTestRule.setContent {
SimpleTestLayout(Modifier.semantics(mergeAllDescendants = true) {}.testTag(tag1)) {
SimpleTestLayout(Modifier.semantics { accessibilityLabel = label1 }) { }
SimpleTestLayout(Modifier.semantics(mergeAllDescendants = true) {}.testTag(tag2)) {
SimpleTestLayout(Modifier.semantics { accessibilityLabel = label2 }) { }
}
}
}
onNodeWithTag(tag1).assertLabelEquals(label1)
onNodeWithTag(tag2).assertLabelEquals(label2)
}
@Test
fun removingMergedSubtree_updatesSemantics() {
val label = "foo"
val showSubtree = mutableStateOf(true)
composeTestRule.setContent {
SimpleTestLayout(Modifier.semantics(mergeAllDescendants = true) {}.testTag(TestTag)) {
if (showSubtree.value) {
SimpleTestLayout(Modifier.semantics { accessibilityLabel = label }) { }
}
}
}
onNodeWithTag(TestTag).assertLabelEquals(label)
runOnIdle { showSubtree.value = false }
onNodeWithTag(TestTag).assertDoesNotHaveProperty(SemanticsProperties.AccessibilityLabel)
onAllNodesWithText(label).assertCountEquals(0)
}
@Test
fun addingNewMergedNode_updatesSemantics() {
val label = "foo"
val value = "bar"
val showNewNode = mutableStateOf(false)
composeTestRule.setContent {
SimpleTestLayout(Modifier.semantics(mergeAllDescendants = true) {}.testTag(TestTag)) {
SimpleTestLayout(Modifier.semantics { accessibilityLabel = label }) { }
if (showNewNode.value) {
SimpleTestLayout(Modifier.semantics { accessibilityValue = value }) { }
}
}
}
onNodeWithTag(TestTag)
.assertLabelEquals(label)
.assertDoesNotHaveProperty(SemanticsProperties.AccessibilityValue)
runOnIdle { showNewNode.value = true }
onNodeWithTag(TestTag)
.assertLabelEquals(label)
.assertValueEquals(value)
}
@Test
fun removingSubtreeWithoutSemanticsAsTopNode_updatesSemantics() {
val label = "foo"
val showSubtree = mutableStateOf(true)
composeTestRule.setContent {
SimpleTestLayout(Modifier.testTag(TestTag)) {
if (showSubtree.value) {
SimpleTestLayout(Modifier.semantics { accessibilityLabel = label }) { }
}
}
}
onAllNodesWithLabel(label).assertCountEquals(1)
runOnIdle {
showSubtree.value = false
}
onAllNodesWithLabel(label).assertCountEquals(0)
}
@Test
fun changingStackedSemanticsComponent_updatesSemantics() {
val beforeLabel = "before"
val afterLabel = "after"
val isAfter = mutableStateOf(false)
composeTestRule.setContent {
SimpleTestLayout(Modifier.testTag(TestTag).semantics {
accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel }
) {}
}
onNodeWithTag(TestTag).assertLabelEquals(beforeLabel)
runOnIdle { isAfter.value = true }
onNodeWithTag(TestTag).assertLabelEquals(afterLabel)
}
@Test
fun changingStackedSemanticsComponent_notTopMost_updatesSemantics() {
val beforeLabel = "before"
val afterLabel = "after"
val isAfter = mutableStateOf(false)
composeTestRule.setContent {
SimpleTestLayout(Modifier.testTag("don't care")) {
SimpleTestLayout(Modifier.testTag(TestTag).semantics {
accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel }
) {}
}
}
onNodeWithTag(TestTag).assertLabelEquals(beforeLabel)
runOnIdle { isAfter.value = true }
onNodeWithTag(TestTag).assertLabelEquals(afterLabel)
}
@Test
fun changingSemantics_belowStackedLayoutNodes_updatesCorrectly() {
val beforeLabel = "before"
val afterLabel = "after"
val isAfter = mutableStateOf(false)
composeTestRule.setContent {
SimpleTestLayout {
SimpleTestLayout {
SimpleTestLayout(Modifier.testTag(TestTag).semantics {
accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel }
) {}
}
}
}
onNodeWithTag(TestTag).assertLabelEquals(beforeLabel)
runOnIdle { isAfter.value = true }
onNodeWithTag(TestTag).assertLabelEquals(afterLabel)
}
@Test
fun changingSemantics_belowNodeMergedThroughBoundary_updatesCorrectly() {
val beforeLabel = "before"
val afterLabel = "after"
val isAfter = mutableStateOf(false)
composeTestRule.setContent {
SimpleTestLayout(Modifier.testTag(TestTag).semantics(mergeAllDescendants = true) {}) {
SimpleTestLayout(Modifier.semantics {
accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel }
) {}
}
}
onNodeWithTag(TestTag).assertLabelEquals(beforeLabel)
runOnIdle { isAfter.value = true }
onNodeWithTag(TestTag).assertLabelEquals(afterLabel)
}
@Test
fun mergeAllDescendants_doesNotCrossLayoutNodesUpward() {
val label = "label"
composeTestRule.setContent {
SimpleTestLayout(Modifier.testTag(TestTag)) {
SimpleTestLayout(Modifier.semantics(mergeAllDescendants = true) {}) {
SimpleTestLayout(Modifier.semantics { accessibilityLabel = label }) { }
}
}
}
onNodeWithTag(TestTag).assertDoesNotHaveProperty(SemanticsProperties.AccessibilityLabel)
onNodeWithLabel(label) // assert exists
}
@Test
fun updateToNodeWithMultipleBoundaryChildren_updatesCorrectly() {
// This test reproduced a bug that caused a ConcurrentModificationException when
// detaching SemanticsNodes
val beforeLabel = "before"
val afterLabel = "after"
val isAfter = mutableStateOf(false)
composeTestRule.setContent {
SimpleTestLayout(Modifier.testTag(TestTag).semantics {
accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel }
) {
SimpleTestLayout(Modifier.semantics { }) { }
SimpleTestLayout(Modifier.semantics { }) { }
}
}
onNodeWithTag(TestTag).assertLabelEquals(beforeLabel)
runOnIdle { isAfter.value = true }
onNodeWithTag(TestTag).assertLabelEquals(afterLabel)
}
@Test
fun changingSemantics_doesNotReplaceNodesBelow() {
// Regression test for b/148606417
var nodeCount = 0
val beforeLabel = "before"
val afterLabel = "after"
// Do different things in an attempt to defeat a sufficiently clever compiler
val beforeAction = { println("this never gets called") }
val afterAction = { println("neither does this") }
val isAfter = mutableStateOf(false)
composeTestRule.setContent {
SimpleTestLayout(Modifier.testTag(TestTag).semantics {
accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel
onClick(action = {
if (isAfter.value) afterAction() else beforeAction()
return@onClick true
})
}) {
SimpleTestLayout {
remember { nodeCount++ }
}
}
}
// This isn't the important part, just makes sure everything is behaving as expected
onNodeWithTag(TestTag).assertLabelEquals(beforeLabel)
assertThat(nodeCount).isEqualTo(1)
runOnIdle { isAfter.value = true }
// Make sure everything is still behaving as expected
onNodeWithTag(TestTag).assertLabelEquals(afterLabel)
// This is the important part: make sure we didn't replace the identity due to unwanted
// pivotal properties
assertThat(nodeCount).isEqualTo(1)
}
}
private fun SemanticsNodeInteraction.assertDoesNotHaveProperty(property: SemanticsPropertyKey<*>) {
assert(SemanticsMatcher.keyNotDefined(property))
}
/**
* A simple test layout that does the bare minimum required to lay out an arbitrary number of
* children reasonably. Useful for Semantics hierarchy testing
*/
@Composable
private fun SimpleTestLayout(modifier: Modifier = Modifier, children: @Composable () -> Unit) {
Layout(modifier = modifier, children = children) { measurables, constraints ->
if (measurables.isEmpty()) {
layout(constraints.minWidth, constraints.minHeight) {}
} else {
val placeables = measurables.map {
it.measure(constraints)
}
val (width, height) = with(placeables.filterNotNull()) {
Pair(
max(
maxByOrNull { it.width }?.width ?: 0,
constraints.minWidth
),
max(
maxByOrNull { it.height }?.height ?: 0,
constraints.minHeight
)
)
}
layout(width, height) {
for (placeable in placeables) {
placeable.place(0, 0)
}
}
}
}
}