[go: nahoru, domu]

blob: 0f2f42d6397c92ff89c5f267309744d794912d6d [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.Composable
import androidx.compose.remember
import androidx.test.filters.MediumTest
import androidx.ui.core.Layout
import androidx.ui.core.test.ValueModel
import androidx.ui.test.SemanticsNodeInteraction
import androidx.ui.test.SemanticsPredicate
import androidx.ui.test.assertCountEquals
import androidx.ui.test.assertLabelEquals
import androidx.ui.test.assertValueEquals
import androidx.ui.test.createComposeRule
import androidx.ui.test.findAllByText
import androidx.ui.test.findByTag
import androidx.ui.test.findByText
import androidx.ui.test.assert
import androidx.ui.unit.IntPx
import androidx.ui.unit.ipx
import androidx.ui.unit.max
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
@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)
composeTestRule.runOnUiThread {
updateFunction()
latch.countDown()
}
latch.await()
}
@Test
fun removingMergedSubtree_updatesSemantics() {
val label = "foo"
val showSubtree = ValueModel(true)
composeTestRule.setContent {
Semantics(container = true, properties = {
testTag = TestTag
}) {
SimpleTestLayout {
if (showSubtree.value) {
Semantics(properties = {
accessibilityLabel = label
}) {
SimpleTestLayout {
}
}
}
}
}
}
findByTag(TestTag).assertLabelEquals(label)
composeTestRule.runOnIdleCompose { showSubtree.value = false }
findByTag(TestTag).assertDoesNotHaveProperty(SemanticsProperties.AccessibilityLabel)
findAllByText(label).assertCountEquals(0)
}
@Test
fun addingNewMergedNode_updatesSemantics() {
val label = "foo"
val value = "bar"
val showNewNode = ValueModel(false)
composeTestRule.setContent {
Semantics(container = true, properties = {
testTag = TestTag
}) {
SimpleTestLayout {
Semantics(properties = {
accessibilityLabel = label
}) {
SimpleTestLayout {
}
}
if (showNewNode.value) {
Semantics(properties = {
accessibilityValue = value
}) {
SimpleTestLayout {
}
}
}
}
}
}
findByTag(TestTag)
.assertLabelEquals(label)
.assertDoesNotHaveProperty(SemanticsProperties.AccessibilityValue)
composeTestRule.runOnIdleCompose { showNewNode.value = true }
findByTag(TestTag)
.assertLabelEquals(label)
.assertValueEquals(value)
}
@Test
fun removingSubtreeWithoutSemanticsAsTopNode_updatesSemantics() {
val label = "foo"
val showSubtree = ValueModel(true)
composeTestRule.setContent {
Semantics(container = true, properties = {
testTag = TestTag
}) {
SimpleTestLayout {
if (showSubtree.value) {
Semantics(container = true, properties = {
accessibilityLabel = "foo"
}) {
SimpleTestLayout {
}
}
}
}
}
}
findAllByText(label).assertCountEquals(1)
composeTestRule.runOnIdleCompose {
showSubtree.value = false
}
findAllByText(label).assertCountEquals(0)
}
@Test
fun changingStackedSemanticsComponent_updatesSemantics() {
val beforeLabel = "before"
val afterLabel = "after"
val isAfter = ValueModel(false)
composeTestRule.setContent {
Semantics(container = true, properties = {
testTag = TestTag
}) {
Semantics(properties = {
accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel
}) {
SimpleTestLayout {
}
}
}
}
findByTag(TestTag).assertLabelEquals(beforeLabel)
composeTestRule.runOnIdleCompose { isAfter.value = true }
findByTag(TestTag).assertLabelEquals(afterLabel)
}
@Test
fun changingStackedSemanticsComponent_notTopMost_updatesSemantics() {
val beforeLabel = "before"
val afterLabel = "after"
val isAfter = ValueModel(false)
composeTestRule.setContent {
Semantics(container = true, properties = { testTag = "don't care" }) {
SimpleTestLayout {
Semantics(container = true, properties = {
testTag = TestTag
}) {
Semantics(properties = {
accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel
}) {
SimpleTestLayout {
}
}
}
}
}
}
findByTag(TestTag).assertLabelEquals(beforeLabel)
composeTestRule.runOnIdleCompose { isAfter.value = true }
findByTag(TestTag).assertLabelEquals(afterLabel)
}
@Test
fun changingSemantics_belowStackedLayoutNodes_updatesCorrectly() {
val beforeLabel = "before"
val afterLabel = "after"
val isAfter = ValueModel(false)
composeTestRule.setContent {
SimpleTestLayout {
SimpleTestLayout {
Semantics(container = true,
properties = {
testTag = TestTag
accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel
}) {
SimpleTestLayout {}
}
}
}
}
findByTag(TestTag).assertLabelEquals(beforeLabel)
composeTestRule.runOnIdleCompose { isAfter.value = true }
findByTag(TestTag).assertLabelEquals(afterLabel)
}
@Test
fun changingSemantics_belowNodeMergedThroughBoundary_updatesCorrectly() {
val beforeLabel = "before"
val afterLabel = "after"
val isAfter = ValueModel(false)
composeTestRule.setContent {
Semantics(properties = { testTag = TestTag }) {
Semantics(container = true, mergeAllDescendants = true) {
SimpleTestLayout {
Semantics(container = true, properties = {
accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel
}) {
SimpleTestLayout {}
}
}
}
}
}
findByTag(TestTag).assertLabelEquals(beforeLabel)
composeTestRule.runOnIdleCompose { isAfter.value = true }
findByTag(TestTag).assertLabelEquals(afterLabel)
}
@Test
fun mergeAllDescendants_doesNotCrossLayoutNodesUpward() {
val label = "label"
composeTestRule.setContent {
Semantics(container = true, properties = { testTag = TestTag }) {
SimpleTestLayout {
Semantics(container = true, mergeAllDescendants = true) {
SimpleTestLayout {
Semantics(container = true, properties = {
accessibilityLabel = label
}) {
SimpleTestLayout {}
}
}
}
}
}
}
findByTag(TestTag).assertDoesNotHaveProperty(SemanticsProperties.AccessibilityLabel)
findByText(label) // assert exists
}
@Test(expected = IllegalArgumentException::class)
fun mergeAllDescendants_withoutBoundary_throws() {
composeTestRule.setContent {
// This is more complicated than required, but makes sure we get the case
// where it's merged down into a node that does have a boundary, as it's still
// not allowed even in this case
Semantics(mergeAllDescendants = true) {
SimpleTestLayout {
Semantics(container = true) {
SimpleTestLayout { }
}
}
}
}
}
@Test
fun updateToNodeWithMultipleBoundaryChildren_updatesCorrectly() {
// This test reproduced a bug that caused a ConcurrentModificationException when
// detaching SemanticsNodes
val beforeLabel = "before"
val afterLabel = "after"
val isAfter = ValueModel(false)
composeTestRule.setContent {
Semantics(container = true, properties = {
accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel
testTag = TestTag
}) {
SimpleTestLayout {
Semantics(container = true) {
SimpleTestLayout { }
}
Semantics(container = true) {
SimpleTestLayout { }
}
}
}
}
findByTag(TestTag).assertLabelEquals(beforeLabel)
composeTestRule.runOnIdleCompose { isAfter.value = true }
findByTag(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 = ValueModel(false)
composeTestRule.setContent {
Semantics(container = true, properties = {
accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel
onClick(action = if (isAfter.value) afterAction else beforeAction)
testTag = TestTag
}) {
SimpleTestLayout {
remember { nodeCount++ }
}
}
}
// This isn't the important part, just makes sure everything is behaving as expected
findByTag(TestTag).assertLabelEquals(beforeLabel)
assertThat(nodeCount).isEqualTo(1)
composeTestRule.runOnIdleCompose { isAfter.value = true }
// Make sure everything is still behaving as expected
findByTag(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(SemanticsPredicate.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(children: @Composable() () -> Unit) {
Layout(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(
maxBy { it.width.value }?.width ?: IntPx.Zero,
constraints.minWidth
),
max(
maxBy { it.height.value }?.height ?: IntPx.Zero,
constraints.minHeight
)
)
}
layout(width, height) {
for (placeable in placeables) {
placeable.place(0.ipx, 0.ipx)
}
}
}
}
}