[go: nahoru, domu]

blob: 9f25017000c4243cf42a231151b09f140d1bc44f [file] [log] [blame]
/*
* Copyright 2023 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.tv.material3
import android.os.Build
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.FocusInteraction
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.testutils.assertContainsColor
import androidx.compose.testutils.assertDoesNotContainColor
import androidx.compose.testutils.assertShape
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsFocused
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performKeyInput
import androidx.compose.ui.test.performSemanticsAction
import androidx.compose.ui.test.pressKey
import androidx.compose.ui.unit.Dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.FlakyTest
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import androidx.tv.material3.tokens.Elevation
import com.google.common.truth.Truth
import kotlin.math.abs
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
private fun assertFloatPrecision(a: Float, b: Float) =
Truth.assertThat(abs(a - b)).isLessThan(0.0001f)
@OptIn(
ExperimentalComposeUiApi::class,
ExperimentalTestApi::class,
ExperimentalTvMaterial3Api::class
)
@MediumTest
@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
class SurfaceTest {
@get:Rule
val rule = createComposeRule()
private fun Int.toDp(): Dp = with(rule.density) { toDp() }
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun originalOrderingWhenTheDefaultElevationIsUsed() {
rule.setContent {
Box(
Modifier
.size(10.toDp())
.semantics(mergeDescendants = true) {}
.testTag("box")
) {
Surface(
onClick = {},
shape = ClickableSurfaceDefaults.shape(
shape = RectangleShape
),
color = ClickableSurfaceDefaults.color(
color = Color.Yellow
)
) {
Box(Modifier.fillMaxSize())
}
Surface(
onClick = {},
shape = ClickableSurfaceDefaults.shape(
shape = RectangleShape
),
color = ClickableSurfaceDefaults.color(
color = Color.Green
)
) {
Box(Modifier.fillMaxSize())
}
}
}
rule.onNodeWithTag("box").captureToImage().assertShape(
density = rule.density,
shape = RectangleShape,
shapeColor = Color.Green,
backgroundColor = Color.White
)
}
@Test
fun absoluteElevationCompositionLocalIsSet() {
var outerElevation: Dp? = null
var innerElevation: Dp? = null
rule.setContent {
Surface(onClick = {}, tonalElevation = 2.toDp()) {
outerElevation = LocalAbsoluteTonalElevation.current
Surface(onClick = {}, tonalElevation = 4.toDp()) {
innerElevation = LocalAbsoluteTonalElevation.current
}
}
}
rule.runOnIdle {
innerElevation?.let { nnInnerElevation ->
assertFloatPrecision(nnInnerElevation.value, 6.toDp().value)
}
outerElevation?.let { nnOuterElevation ->
assertFloatPrecision(nnOuterElevation.value, 2.toDp().value)
}
}
}
/**
* Tests that composed modifiers applied to Surface are applied within the changes to
* [LocalContentColor], so they can consume the updated values.
*/
@Test
fun contentColorSetBeforeModifier() {
var contentColor: Color = Color.Unspecified
val expectedColor = Color.Blue
rule.setContent {
CompositionLocalProvider(LocalContentColor provides Color.Red) {
Surface(
modifier = Modifier.composed {
contentColor = LocalContentColor.current
Modifier
},
onClick = {},
tonalElevation = 2.toDp(),
contentColor = ClickableSurfaceDefaults.color(color = expectedColor)
) {}
}
}
rule.runOnIdle {
Truth.assertThat(contentColor).isEqualTo(expectedColor)
}
}
@Test
fun clickableOverload_semantics() {
val count = mutableStateOf(0)
rule.setContent {
Surface(
modifier = Modifier
.testTag("surface"),
onClick = { count.value += 1 }
) {
Text("${count.value}")
Spacer(Modifier.size(30.toDp()))
}
}
rule.onNodeWithTag("surface")
.performSemanticsAction(SemanticsActions.RequestFocus)
.assertHasClickAction()
.assertIsEnabled()
// since we merge descendants we should have text on the same node
.assertTextEquals("0")
.performKeyInput { pressKey(Key.DirectionCenter) }
.assertTextEquals("1")
}
@Test
fun clickableOverload_customSemantics() {
val count = mutableStateOf(0)
rule.setContent {
Surface(
modifier = Modifier
.semantics { role = Role.Tab }
.testTag("surface"),
onClick = { count.value += 1 },
) {
Text("${count.value}")
Spacer(Modifier.size(30.toDp()))
}
}
rule.onNodeWithTag("surface")
.performSemanticsAction(SemanticsActions.RequestFocus)
.assertHasClickAction()
.assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Tab))
.assertIsEnabled()
// since we merge descendants we should have text on the same node
.assertTextEquals("0")
.performKeyInput { pressKey(Key.DirectionCenter) }
.assertTextEquals("1")
}
@Test
fun clickableOverload_clickAction() {
val count = mutableStateOf(0)
rule.setContent {
Surface(
modifier = Modifier
.testTag("surface"),
onClick = { count.value += 1 }
) {
Spacer(modifier = Modifier.size(30.toDp()))
}
}
rule.onNodeWithTag("surface")
.performSemanticsAction(SemanticsActions.RequestFocus)
.performKeyInput { pressKey(Key.DirectionCenter) }
Truth.assertThat(count.value).isEqualTo(1)
rule.onNodeWithTag("surface").performKeyInput { pressKey(Key.DirectionCenter) }
.performKeyInput { pressKey(Key.DirectionCenter) }
Truth.assertThat(count.value).isEqualTo(3)
}
@Test
fun clickableSurface_onDisable_clickFails() {
val count = mutableStateOf(0f)
val enabled = mutableStateOf(true)
rule.setContent {
Surface(
modifier = Modifier
.testTag("surface"),
onClick = { count.value += 1 },
enabled = enabled.value
) {
Spacer(Modifier.size(30.toDp()))
}
}
rule.onNodeWithTag("surface")
.performSemanticsAction(SemanticsActions.RequestFocus)
.assertIsEnabled()
.performKeyInput { pressKey(Key.DirectionCenter) }
Truth.assertThat(count.value).isEqualTo(1)
rule.runOnIdle {
enabled.value = false
}
rule.onNodeWithTag("surface")
.assertIsNotEnabled()
.performKeyInput { pressKey(Key.DirectionCenter) }
.performKeyInput { pressKey(Key.DirectionCenter) }
Truth.assertThat(count.value).isEqualTo(1)
}
@Test
fun clickableOverload_interactionSource() {
val interactionSource = MutableInteractionSource()
lateinit var scope: CoroutineScope
rule.setContent {
scope = rememberCoroutineScope()
Surface(
modifier = Modifier
.testTag("surface"),
onClick = {},
interactionSource = interactionSource
) {
Spacer(Modifier.size(30.toDp()))
}
}
val interactions = mutableListOf<Interaction>()
scope.launch {
interactionSource.interactions.collect { interactions.add(it) }
}
rule.runOnIdle {
Truth.assertThat(interactions).isEmpty()
}
rule.onNodeWithTag("surface")
.performSemanticsAction(SemanticsActions.RequestFocus)
.performKeyInput { keyDown(Key.DirectionCenter) }
rule.runOnIdle {
Truth.assertThat(interactions).hasSize(2)
Truth.assertThat(interactions[1]).isInstanceOf(PressInteraction.Press::class.java)
}
rule.onNodeWithTag("surface").performKeyInput { keyUp(Key.DirectionCenter) }
rule.runOnIdle {
Truth.assertThat(interactions).hasSize(3)
Truth.assertThat(interactions.first()).isInstanceOf(FocusInteraction.Focus::class.java)
Truth.assertThat(interactions[1]).isInstanceOf(PressInteraction.Press::class.java)
Truth.assertThat(interactions[2]).isInstanceOf(PressInteraction.Release::class.java)
Truth.assertThat((interactions[2] as PressInteraction.Release).press)
.isEqualTo(interactions[1])
}
}
@Test
fun clickableSurface_reactsToStateChange() {
val interactionSource = MutableInteractionSource()
var isPressed by mutableStateOf(false)
rule.setContent {
isPressed = interactionSource.collectIsPressedAsState().value
Surface(
modifier = Modifier
.testTag("surface")
.size(100.toDp()),
onClick = {},
interactionSource = interactionSource
) {}
}
with(rule.onNodeWithTag("surface")) {
performSemanticsAction(SemanticsActions.RequestFocus)
assertIsFocused()
performKeyInput { keyDown(Key.DirectionCenter) }
}
rule.waitUntil(condition = { isPressed })
Truth.assertThat(isPressed).isTrue()
}
@FlakyTest(bugId = 269229262)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun clickableSurface_onFocus_changesGlowColor() {
rule.setContent {
Surface(
modifier = Modifier
.testTag("surface")
.size(100.toDp()),
onClick = {},
color = ClickableSurfaceDefaults.color(
color = Color.Transparent,
focusedColor = Color.Transparent
),
glow = ClickableSurfaceDefaults.glow(
glow = Glow(
elevationColor = Color.Magenta,
elevation = Elevation.Level5
),
focusedGlow = Glow(
elevationColor = Color.Green,
elevation = Elevation.Level5
)
)
) {}
}
rule.onNodeWithTag("surface")
.captureToImage()
.assertContainsColor(Color.Magenta)
rule.onNodeWithTag("surface")
.performSemanticsAction(SemanticsActions.RequestFocus)
rule.onNodeWithTag("surface")
.captureToImage()
.assertContainsColor(Color.Green)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun clickableSurface_onFocus_changesScaleFactor() {
rule.setContent {
Box(
modifier = Modifier
.background(Color.Blue)
.size(50.toDp())
)
Surface(
onClick = {},
modifier = Modifier
.size(50.toDp())
.testTag("surface"),
scale = ClickableSurfaceDefaults.scale(
focusedScale = 1.5f
)
) {}
}
rule.onRoot().captureToImage().assertContainsColor(Color.Blue)
rule.onNodeWithTag("surface").performSemanticsAction(SemanticsActions.RequestFocus)
rule.onRoot().captureToImage().assertDoesNotContainColor(Color.Blue)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun clickableSurface_onFocus_showsBorder() {
rule.setContent {
Surface(
onClick = { /* Do something */ },
modifier = Modifier
.size(100.toDp())
.testTag("surface"),
border = ClickableSurfaceDefaults.border(
focusedBorder = Border(
border = BorderStroke(width = 5.toDp(), color = Color.Magenta)
)
),
color = ClickableSurfaceDefaults.color(
color = Color.Transparent,
focusedColor = Color.Transparent
)
) {}
}
val surface = rule.onNodeWithTag("surface")
surface.captureToImage().assertDoesNotContainColor(Color.Magenta)
surface.performSemanticsAction(SemanticsActions.RequestFocus)
surface.captureToImage().assertContainsColor(Color.Magenta)
}
@Test
fun toggleable_semantics() {
var isChecked by mutableStateOf(false)
rule.setContent {
Surface(
checked = isChecked,
modifier = Modifier
.testTag("surface"),
onCheckedChange = { isChecked = it }
) {
Text("$isChecked")
Spacer(Modifier.size(30.toDp()))
}
}
rule.onNodeWithTag("surface")
.performSemanticsAction(SemanticsActions.RequestFocus)
.assertHasClickAction()
.assert(SemanticsMatcher.keyNotDefined(SemanticsProperties.Role))
.assertIsEnabled()
// since we merge descendants we should have text on the same node
.assertTextEquals("false")
.performKeyInput { pressKey(Key.DirectionCenter) }
.assertTextEquals("true")
}
@Test
fun toggleable_customSemantics() {
var isChecked by mutableStateOf(false)
rule.setContent {
Surface(
checked = isChecked,
modifier = Modifier
.semantics { role = Role.Tab }
.testTag("surface"),
onCheckedChange = { isChecked = it }
) {
Text("$isChecked")
Spacer(Modifier.size(30.toDp()))
}
}
rule.onNodeWithTag("surface")
.performSemanticsAction(SemanticsActions.RequestFocus)
.assertHasClickAction()
.assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Tab))
.assertIsEnabled()
// since we merge descendants we should have text on the same node
.assertTextEquals("false")
.performKeyInput { pressKey(Key.DirectionCenter) }
.assertTextEquals("true")
}
@Test
fun toggleable_toggleAction() {
var isChecked by mutableStateOf(false)
rule.setContent {
Surface(
checked = isChecked,
modifier = Modifier
.testTag("surface"),
onCheckedChange = { isChecked = it }
) {
Spacer(modifier = Modifier.size(30.toDp()))
}
}
rule.onNodeWithTag("surface")
.performSemanticsAction(SemanticsActions.RequestFocus)
.performKeyInput { pressKey(Key.DirectionCenter) }
Truth.assertThat(isChecked).isTrue()
rule.onNodeWithTag("surface").performKeyInput { pressKey(Key.DirectionCenter) }
Truth.assertThat(isChecked).isFalse()
}
@Test
fun toggleable_enabled_disabled() {
var isChecked by mutableStateOf(false)
var enabled by mutableStateOf(true)
rule.setContent {
Surface(
checked = isChecked,
modifier = Modifier
.testTag("surface"),
onCheckedChange = { isChecked = it },
enabled = enabled
) {
Spacer(Modifier.size(30.toDp()))
}
}
rule.onNodeWithTag("surface")
.performSemanticsAction(SemanticsActions.RequestFocus)
.assertIsEnabled()
.performKeyInput { pressKey(Key.DirectionCenter) }
Truth.assertThat(isChecked).isTrue()
rule.runOnIdle {
enabled = false
}
rule.onNodeWithTag("surface")
.assertIsNotEnabled()
.performKeyInput { pressKey(Key.DirectionCenter) }
Truth.assertThat(isChecked).isTrue()
}
@Test
fun toggleable_interactionSource() {
val interactionSource = MutableInteractionSource()
lateinit var scope: CoroutineScope
rule.setContent {
scope = rememberCoroutineScope()
Surface(
checked = false,
modifier = Modifier
.testTag("surface"),
onCheckedChange = {},
interactionSource = interactionSource
) {
Spacer(Modifier.size(30.toDp()))
}
}
val interactions = mutableListOf<Interaction>()
scope.launch {
interactionSource.interactions.collect { interactions.add(it) }
}
rule.runOnIdle {
Truth.assertThat(interactions).isEmpty()
}
rule.onNodeWithTag("surface")
.performSemanticsAction(SemanticsActions.RequestFocus)
.performKeyInput { keyDown(Key.DirectionCenter) }
rule.runOnIdle {
Truth.assertThat(interactions).hasSize(2)
Truth.assertThat(interactions[1]).isInstanceOf(PressInteraction.Press::class.java)
}
rule.onNodeWithTag("surface").performKeyInput { keyUp(Key.DirectionCenter) }
rule.runOnIdle {
Truth.assertThat(interactions).hasSize(3)
Truth.assertThat(interactions.first()).isInstanceOf(FocusInteraction.Focus::class.java)
Truth.assertThat(interactions[1]).isInstanceOf(PressInteraction.Press::class.java)
Truth.assertThat(interactions[2]).isInstanceOf(PressInteraction.Release::class.java)
Truth.assertThat((interactions[2] as PressInteraction.Release).press)
.isEqualTo(interactions[1])
}
}
@Test
fun toggleableSurface_reactsToStateChange() {
val interactionSource = MutableInteractionSource()
var isPressed by mutableStateOf(false)
rule.setContent {
isPressed = interactionSource.collectIsPressedAsState().value
Surface(
checked = false,
modifier = Modifier
.testTag("surface")
.size(100.toDp()),
onCheckedChange = {},
interactionSource = interactionSource
) {}
}
with(rule.onNodeWithTag("surface")) {
performSemanticsAction(SemanticsActions.RequestFocus)
assertIsFocused()
performKeyInput { keyDown(Key.DirectionCenter) }
}
rule.waitUntil(condition = { isPressed })
Truth.assertThat(isPressed).isTrue()
}
@FlakyTest(bugId = 269229262)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun toggleableSurface_onCheckedChange_changesGlowColor() {
var isChecked by mutableStateOf(false)
var focusManager: FocusManager? = null
rule.setContent {
focusManager = LocalFocusManager.current
Surface(
checked = isChecked,
modifier = Modifier
.testTag("surface")
.size(100.toDp()),
onCheckedChange = { isChecked = it },
color = ToggleableSurfaceDefaults.color(
color = Color.Transparent,
selectedColor = Color.Transparent
),
glow = ToggleableSurfaceDefaults.glow(
glow = Glow(
elevationColor = Color.Magenta,
elevation = Elevation.Level5
),
selectedGlow = Glow(
elevationColor = Color.Green,
elevation = Elevation.Level5
)
)
) {}
}
rule.onNodeWithTag("surface")
.captureToImage()
.assertContainsColor(Color.Magenta)
rule.onNodeWithTag("surface")
.performSemanticsAction(SemanticsActions.RequestFocus)
.performKeyInput { pressKey(Key.DirectionCenter) }
// Remove focused state to reveal selected state
focusManager?.clearFocus()
rule.onNodeWithTag("surface")
.captureToImage()
.assertContainsColor(Color.Green)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun toggleableSurface_onCheckedChange_changesScaleFactor() {
var isChecked by mutableStateOf(false)
var focusManager: FocusManager? = null
rule.setContent {
focusManager = LocalFocusManager.current
Box(
modifier = Modifier
.background(Color.Blue)
.size(50.toDp())
)
Surface(
checked = isChecked,
onCheckedChange = { isChecked = it },
modifier = Modifier
.size(50.toDp())
.testTag("surface"),
scale = ToggleableSurfaceDefaults.scale(
selectedScale = 1.5f
)
) {}
}
rule.onRoot().captureToImage().assertContainsColor(Color.Blue)
rule.onNodeWithTag("surface")
.performSemanticsAction(SemanticsActions.RequestFocus)
.performKeyInput { pressKey(Key.DirectionCenter) }
// Remove focused state to reveal selected state
focusManager?.clearFocus()
rule.onRoot().captureToImage().assertDoesNotContainColor(Color.Blue)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun toggleableSurface_onCheckedChange_showsOutline() {
var isChecked by mutableStateOf(false)
var focusManager: FocusManager? = null
rule.setContent {
focusManager = LocalFocusManager.current
Surface(
checked = isChecked,
onCheckedChange = { isChecked = it },
modifier = Modifier
.size(100.toDp())
.testTag("surface"),
border = ToggleableSurfaceDefaults.border(
selectedBorder = Border(
border = BorderStroke(width = 5.toDp(), color = Color.Magenta)
)
),
color = ToggleableSurfaceDefaults.color(
color = Color.Transparent,
selectedColor = Color.Transparent
)
) {}
}
val surface = rule.onNodeWithTag("surface")
surface.captureToImage().assertDoesNotContainColor(Color.Magenta)
surface
.performSemanticsAction(SemanticsActions.RequestFocus)
.performKeyInput { pressKey(Key.DirectionCenter) }
// Remove focused state to reveal selected state
focusManager?.clearFocus()
surface.captureToImage().assertContainsColor(Color.Magenta)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun surfaceChangesStyleOnChangingEnabledState() {
var surfaceEnabled by mutableStateOf(true)
rule.setContent {
Surface(
modifier = Modifier
.size(100.toDp())
.testTag("surface"),
onClick = {},
enabled = surfaceEnabled,
color = ClickableSurfaceDefaults.color(
color = Color.Green,
disabledColor = Color.Red
)
) {}
}
// Assert surface is enabled
rule.onNodeWithTag("surface").captureToImage().assertContainsColor(Color.Green)
surfaceEnabled = false
// Assert surface is disabled
rule.onNodeWithTag("surface").captureToImage().assertContainsColor(Color.Red)
}
}