[go: nahoru, domu]

blob: 2eed1ff7d60cea7cae0b9bb1054a24b44a7a5cc9 [file] [log] [blame]
/*
* Copyright 2021 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.compose.material3
import android.os.Build
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.tokens.BottomAppBarTokens
import androidx.compose.material3.tokens.TopAppBarLargeTokens
import androidx.compose.material3.tokens.TopAppBarMediumTokens
import androidx.compose.material3.tokens.TopAppBarSmallCenteredTokens
import androidx.compose.material3.tokens.TopAppBarSmallTokens
import androidx.compose.runtime.Composable
import androidx.compose.testutils.assertContainsColor
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertHeightIsEqualTo
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEqualTo
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
import androidx.compose.ui.test.assertWidthIsEqualTo
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.getUnclippedBoundsInRoot
import androidx.compose.ui.test.junit4.StateRestorationTester
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onLast
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeLeft
import androidx.compose.ui.test.swipeRight
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.height
import androidx.compose.ui.unit.width
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@OptIn(ExperimentalMaterial3Api::class)
@MediumTest
@RunWith(AndroidJUnit4::class)
class AppBarTest {
@get:Rule
val rule = createComposeRule()
@Test
fun smallTopAppBar_expandsToScreen() {
rule
.setMaterialContentForSizeAssertions {
SmallTopAppBar(title = { Text("Title") })
}
.assertHeightIsEqualTo(TopAppBarSmallTokens.ContainerHeight)
.assertWidthIsEqualTo(rule.rootWidth())
}
@Test
fun smallTopAppBar_withTitle() {
val title = "Title"
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(TopAppBarTestTag)) {
SmallTopAppBar(title = { Text(title) })
}
}
rule.onNodeWithText(title).assertIsDisplayed()
}
@Test
fun smallTopAppBar_default_positioning() {
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(TopAppBarTestTag)) {
SmallTopAppBar(
navigationIcon = {
FakeIcon(Modifier.testTag(NavigationIconTestTag))
},
title = {
Text("Title", Modifier.testTag(TitleTestTag))
},
actions = {
FakeIcon(Modifier.testTag(ActionsTestTag))
}
)
}
}
assertSmallDefaultPositioning()
}
@Test
fun smallTopAppBar_noNavigationIcon_positioning() {
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(TopAppBarTestTag)) {
SmallTopAppBar(
title = {
Text("Title", Modifier.testTag(TitleTestTag))
},
actions = {
FakeIcon(Modifier.testTag(ActionsTestTag))
}
)
}
}
assertSmallPositioningWithoutNavigation()
}
@Test
fun smallTopAppBar_titleDefaultStyle() {
var textStyle: TextStyle? = null
var expectedTextStyle: TextStyle? = null
rule.setMaterialContent(lightColorScheme()) {
SmallTopAppBar(title = {
Text("Title")
textStyle = LocalTextStyle.current
expectedTextStyle =
MaterialTheme.typography.fromToken(TopAppBarSmallTokens.HeadlineFont)
}
)
}
assertThat(textStyle).isNotNull()
assertThat(textStyle).isEqualTo(expectedTextStyle)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun smallTopAppBar_contentColor() {
var titleColor: Color = Color.Unspecified
var navigationIconColor: Color = Color.Unspecified
var actionsColor: Color = Color.Unspecified
var expectedTitleColor: Color = Color.Unspecified
var expectedNavigationIconColor: Color = Color.Unspecified
var expectedActionsColor: Color = Color.Unspecified
var expectedContainerColor: Color = Color.Unspecified
rule.setMaterialContent(lightColorScheme()) {
SmallTopAppBar(
modifier = Modifier.testTag(TopAppBarTestTag),
navigationIcon = {
FakeIcon(Modifier.testTag(NavigationIconTestTag))
navigationIconColor = LocalContentColor.current
expectedNavigationIconColor =
TopAppBarDefaults.smallTopAppBarColors().navigationIconContentColor
// fraction = 0f to indicate no scroll.
expectedContainerColor = TopAppBarDefaults
.smallTopAppBarColors()
.containerColor(colorTransitionFraction = 0f)
},
title = {
Text("Title", Modifier.testTag(TitleTestTag))
titleColor = LocalContentColor.current
expectedTitleColor = TopAppBarDefaults
.smallTopAppBarColors().titleContentColor
},
actions = {
FakeIcon(Modifier.testTag(ActionsTestTag))
actionsColor = LocalContentColor.current
expectedActionsColor = TopAppBarDefaults
.smallTopAppBarColors().actionIconContentColor
}
)
}
assertThat(navigationIconColor).isNotNull()
assertThat(titleColor).isNotNull()
assertThat(actionsColor).isNotNull()
assertThat(navigationIconColor).isEqualTo(expectedNavigationIconColor)
assertThat(titleColor).isEqualTo(expectedTitleColor)
assertThat(actionsColor).isEqualTo(expectedActionsColor)
rule.onNodeWithTag(TopAppBarTestTag).captureToImage()
.assertContainsColor(expectedContainerColor)
}
@OptIn(ExperimentalMaterial3Api::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun smallTopAppBar_scrolledContentColor() {
var expectedScrolledContainerColor: Color = Color.Unspecified
lateinit var scrollBehavior: TopAppBarScrollBehavior
rule.setMaterialContent(lightColorScheme()) {
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
SmallTopAppBar(
modifier = Modifier.testTag(TopAppBarTestTag),
title = {
Text("Title", Modifier.testTag(TitleTestTag))
// fraction = 1f to indicate a scroll.
expectedScrolledContainerColor =
TopAppBarDefaults.smallTopAppBarColors()
.containerColor(colorTransitionFraction = 1f)
},
scrollBehavior = scrollBehavior
)
}
// Simulate scrolled content.
rule.runOnIdle {
scrollBehavior.state.contentOffset = -100f
}
rule.waitForIdle()
rule.onNodeWithTag(TopAppBarTestTag).captureToImage()
.assertContainsColor(expectedScrolledContainerColor)
}
@OptIn(ExperimentalMaterial3Api::class)
@Test
fun smallTopAppBar_scrolledPositioning() {
lateinit var scrollBehavior: TopAppBarScrollBehavior
val scrollHeightOffsetDp = 20.dp
var scrollHeightOffsetPx = 0f
rule.setMaterialContent(lightColorScheme()) {
scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
scrollHeightOffsetPx = with(LocalDensity.current) { scrollHeightOffsetDp.toPx() }
SmallTopAppBar(
modifier = Modifier.testTag(TopAppBarTestTag),
title = { Text("Title", Modifier.testTag(TitleTestTag)) },
scrollBehavior = scrollBehavior
)
}
// Simulate scrolled content.
rule.runOnIdle {
scrollBehavior.state.heightOffset = -scrollHeightOffsetPx
scrollBehavior.state.contentOffset = -scrollHeightOffsetPx
}
rule.waitForIdle()
rule.onNodeWithTag(TopAppBarTestTag)
.assertHeightIsEqualTo(TopAppBarSmallTokens.ContainerHeight - scrollHeightOffsetDp)
}
@Test
fun centerAlignedTopAppBar_expandsToScreen() {
rule.setMaterialContentForSizeAssertions {
CenterAlignedTopAppBar(title = { Text("Title") })
}
.assertHeightIsEqualTo(TopAppBarSmallCenteredTokens.ContainerHeight)
.assertWidthIsEqualTo(rule.rootWidth())
}
@Test
fun centerAlignedTopAppBar_withTitle() {
val title = "Title"
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(TopAppBarTestTag)) {
CenterAlignedTopAppBar(title = { Text(title) })
}
}
rule.onNodeWithText(title).assertIsDisplayed()
}
@Test
fun centerAlignedTopAppBar_default_positioning() {
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(TopAppBarTestTag)) {
CenterAlignedTopAppBar(
navigationIcon = {
FakeIcon(Modifier.testTag(NavigationIconTestTag))
},
title = {
Text("Title", Modifier.testTag(TitleTestTag))
},
actions = {
FakeIcon(Modifier.testTag(ActionsTestTag))
}
)
}
}
assertSmallDefaultPositioning(isCenteredTitle = true)
}
@Test
fun centerAlignedTopAppBar_default_positioning_respectsWindowInsets() {
val padding = 10.dp
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(TopAppBarTestTag)) {
CenterAlignedTopAppBar(
navigationIcon = {
FakeIcon(Modifier.testTag(NavigationIconTestTag))
},
title = {
Text("Title", Modifier.testTag(TitleTestTag))
},
actions = {
FakeIcon(Modifier.testTag(ActionsTestTag))
},
windowInsets = WindowInsets(padding, padding, padding, padding)
)
}
}
val appBarBounds = rule.onNodeWithTag(TopAppBarTestTag).getUnclippedBoundsInRoot()
val appBarBottomEdgeY = appBarBounds.top + appBarBounds.height
rule.onNodeWithTag(NavigationIconTestTag)
// Navigation icon should be 4.dp from the start
.assertLeftPositionInRootIsEqualTo(AppBarStartAndEndPadding + padding)
// Navigation icon should be centered within the height of the app bar.
.assertTopPositionInRootIsEqualTo(
appBarBottomEdgeY - AppBarTopAndBottomPadding - padding - FakeIconSize
)
}
@Test
fun centerAlignedTopAppBar_noNavigationIcon_positioning() {
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(TopAppBarTestTag)) {
CenterAlignedTopAppBar(
title = {
Text("Title", Modifier.testTag(TitleTestTag))
},
actions = {
FakeIcon(Modifier.testTag(ActionsTestTag))
}
)
}
}
assertSmallPositioningWithoutNavigation(isCenteredTitle = true)
}
@Test
fun centerAlignedTopAppBar_titleDefaultStyle() {
var textStyle: TextStyle? = null
var expectedTextStyle: TextStyle? = null
rule.setMaterialContent(lightColorScheme()) {
CenterAlignedTopAppBar(
title = {
Text("Title")
textStyle = LocalTextStyle.current
expectedTextStyle =
MaterialTheme.typography.fromToken(
TopAppBarSmallCenteredTokens.HeadlineFont
)
}
)
}
assertThat(textStyle).isNotNull()
assertThat(textStyle).isEqualTo(expectedTextStyle)
}
@Test
fun centerAlignedTopAppBar_measureWithNonZeroMinWidth() {
var appBarSize = IntSize.Zero
rule.setMaterialContent(lightColorScheme()) {
CenterAlignedTopAppBar(
modifier = Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(
constraints.copy(minWidth = constraints.maxWidth)
)
appBarSize = IntSize(placeable.width, placeable.height)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
},
title = {
Text("Title")
}
)
}
assertThat(appBarSize).isNotEqualTo(IntSize.Zero)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun centerAlignedTopAppBar_contentColor() {
var titleColor: Color = Color.Unspecified
var navigationIconColor: Color = Color.Unspecified
var actionsColor: Color = Color.Unspecified
var expectedTitleColor: Color = Color.Unspecified
var expectedNavigationIconColor: Color = Color.Unspecified
var expectedActionsColor: Color = Color.Unspecified
var expectedContainerColor: Color = Color.Unspecified
rule.setMaterialContent(lightColorScheme()) {
CenterAlignedTopAppBar(
modifier = Modifier.testTag(TopAppBarTestTag),
navigationIcon = {
FakeIcon(Modifier.testTag(NavigationIconTestTag))
navigationIconColor = LocalContentColor.current
expectedNavigationIconColor =
TopAppBarDefaults.centerAlignedTopAppBarColors()
.navigationIconContentColor
// fraction = 0f to indicate no scroll.
expectedContainerColor =
TopAppBarDefaults.centerAlignedTopAppBarColors()
.containerColor(colorTransitionFraction = 0f)
},
title = {
Text("Title", Modifier.testTag(TitleTestTag))
titleColor = LocalContentColor.current
expectedTitleColor =
TopAppBarDefaults.centerAlignedTopAppBarColors()
.titleContentColor
},
actions = {
FakeIcon(Modifier.testTag(ActionsTestTag))
actionsColor = LocalContentColor.current
expectedActionsColor =
TopAppBarDefaults.centerAlignedTopAppBarColors()
.actionIconContentColor
}
)
}
assertThat(navigationIconColor).isNotNull()
assertThat(titleColor).isNotNull()
assertThat(actionsColor).isNotNull()
assertThat(navigationIconColor).isEqualTo(expectedNavigationIconColor)
assertThat(titleColor).isEqualTo(expectedTitleColor)
assertThat(actionsColor).isEqualTo(expectedActionsColor)
rule.onNodeWithTag(TopAppBarTestTag).captureToImage()
.assertContainsColor(expectedContainerColor)
}
@OptIn(ExperimentalMaterial3Api::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun centerAlignedTopAppBar_scrolledContentColor() {
var expectedScrolledContainerColor: Color = Color.Unspecified
lateinit var scrollBehavior: TopAppBarScrollBehavior
rule.setMaterialContent(lightColorScheme()) {
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
CenterAlignedTopAppBar(
modifier = Modifier.testTag(TopAppBarTestTag),
title = {
Text("Title", Modifier.testTag(TitleTestTag))
// fraction = 1f to indicate a scroll.
expectedScrolledContainerColor =
TopAppBarDefaults.centerAlignedTopAppBarColors()
.containerColor(colorTransitionFraction = 1f)
},
scrollBehavior = scrollBehavior
)
}
// Simulate scrolled content.
rule.runOnIdle {
scrollBehavior.state.contentOffset = -100f
}
rule.waitForIdle()
rule.onNodeWithTag(TopAppBarTestTag).captureToImage()
.assertContainsColor(expectedScrolledContainerColor)
}
@Test
fun mediumTopAppBar_expandsToScreen() {
rule.setMaterialContentForSizeAssertions {
MediumTopAppBar(title = { Text("Medium Title") })
}
.assertHeightIsEqualTo(TopAppBarMediumTokens.ContainerHeight)
.assertWidthIsEqualTo(rule.rootWidth())
}
@Test
fun mediumTopAppBar_expanded_positioning() {
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(TopAppBarTestTag)) {
MediumTopAppBar(
navigationIcon = {
FakeIcon(Modifier.testTag(NavigationIconTestTag))
},
title = {
Text("Title", Modifier.testTag(TitleTestTag))
},
actions = {
FakeIcon(Modifier.testTag(ActionsTestTag))
}
)
}
}
// The bottom text baseline should be 24.dp from the bottom of the app bar.
assertMediumOrLargeDefaultPositioning(
expectedAppBarHeight = TopAppBarMediumTokens.ContainerHeight,
bottomTextPadding = 24.dp
)
}
@Test
fun mediumTopAppBar_scrolled_positioning() {
val windowInsets = WindowInsets(13.dp, 13.dp, 13.dp, 13.dp)
val content = @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
Box(Modifier.testTag(TopAppBarTestTag)) {
MediumTopAppBar(
navigationIcon = {
FakeIcon(Modifier.testTag(NavigationIconTestTag))
},
title = {
Text("Title", Modifier.testTag(TitleTestTag))
},
actions = {
FakeIcon(Modifier.testTag(ActionsTestTag))
},
scrollBehavior = scrollBehavior,
windowInsets = windowInsets
)
}
}
assertMediumOrLargeScrolledHeight(
TopAppBarMediumTokens.ContainerHeight,
TopAppBarSmallTokens.ContainerHeight,
windowInsets,
content
)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun mediumTopAppBar_scrolledContainerColor() {
val content = @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
MediumTopAppBar(
modifier = Modifier.testTag(TopAppBarTestTag),
title = {
Text("Title", Modifier.testTag(TitleTestTag))
},
scrollBehavior = scrollBehavior
)
}
assertMediumOrLargeScrolledColors(
TopAppBarMediumTokens.ContainerHeight,
TopAppBarSmallTokens.ContainerHeight,
content
)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun mediumTopAppBar_semantics() {
val content = @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
MediumTopAppBar(
modifier = Modifier.testTag(TopAppBarTestTag),
title = {
Text("Title", Modifier.testTag(TitleTestTag))
},
scrollBehavior = scrollBehavior
)
}
assertMediumOrLargeScrolledSemantics(
TopAppBarMediumTokens.ContainerHeight,
TopAppBarSmallTokens.ContainerHeight,
content
)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun largeTopAppBar_semantics() {
val content = @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
LargeTopAppBar(
modifier = Modifier.testTag(TopAppBarTestTag),
title = {
Text("Title", Modifier.testTag(TitleTestTag))
},
scrollBehavior = scrollBehavior
)
}
assertMediumOrLargeScrolledSemantics(
TopAppBarLargeTokens.ContainerHeight,
TopAppBarSmallTokens.ContainerHeight,
content
)
}
@Test
fun largeTopAppBar_expandsToScreen() {
rule.setMaterialContentForSizeAssertions {
LargeTopAppBar(title = { Text("Large Title") })
}
.assertHeightIsEqualTo(TopAppBarLargeTokens.ContainerHeight)
.assertWidthIsEqualTo(rule.rootWidth())
}
@Test
fun largeTopAppBar_expanded_positioning() {
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(TopAppBarTestTag)) {
LargeTopAppBar(
navigationIcon = {
FakeIcon(Modifier.testTag(NavigationIconTestTag))
},
title = {
Text("Title", Modifier.testTag(TitleTestTag))
},
actions = {
FakeIcon(Modifier.testTag(ActionsTestTag))
}
)
}
}
// The bottom text baseline should be 28.dp from the bottom of the app bar.
assertMediumOrLargeDefaultPositioning(
expectedAppBarHeight = TopAppBarLargeTokens.ContainerHeight,
bottomTextPadding = 28.dp
)
}
@Test
fun largeTopAppBar_scrolled_positioning() {
val windowInsets = WindowInsets(4.dp, 4.dp, 4.dp, 4.dp)
val content = @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
Box(Modifier.testTag(TopAppBarTestTag)) {
LargeTopAppBar(
navigationIcon = {
FakeIcon(Modifier.testTag(NavigationIconTestTag))
},
title = {
Text("Title", Modifier.testTag(TitleTestTag))
},
actions = {
FakeIcon(Modifier.testTag(ActionsTestTag))
},
scrollBehavior = scrollBehavior,
windowInsets = windowInsets
)
}
}
assertMediumOrLargeScrolledHeight(
TopAppBarLargeTokens.ContainerHeight,
TopAppBarSmallTokens.ContainerHeight,
windowInsets,
content
)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun largeTopAppBar_scrolledContainerColor() {
val content = @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
LargeTopAppBar(
modifier = Modifier.testTag(TopAppBarTestTag),
title = {
Text("Title", Modifier.testTag(TitleTestTag))
},
scrollBehavior = scrollBehavior,
)
}
assertMediumOrLargeScrolledColors(
TopAppBarLargeTokens.ContainerHeight,
TopAppBarSmallTokens.ContainerHeight,
content
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Test
fun topAppBar_enterAlways_allowHorizontalScroll() {
lateinit var state: LazyListState
rule.setMaterialContent(lightColorScheme()) {
state = rememberLazyListState()
MultiPageContent(TopAppBarDefaults.enterAlwaysScrollBehavior(), state)
}
rule.onNodeWithTag(LazyListTag).performTouchInput { swipeLeft() }
rule.runOnIdle {
assertThat(state.firstVisibleItemIndex).isEqualTo(1)
}
rule.onNodeWithTag(LazyListTag).performTouchInput { swipeRight() }
rule.runOnIdle {
assertThat(state.firstVisibleItemIndex).isEqualTo(0)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Test
fun topAppBar_exitUntilCollapsed_allowHorizontalScroll() {
lateinit var state: LazyListState
rule.setMaterialContent(lightColorScheme()) {
state = rememberLazyListState()
MultiPageContent(TopAppBarDefaults.exitUntilCollapsedScrollBehavior(), state)
}
rule.onNodeWithTag(LazyListTag).performTouchInput { swipeLeft() }
rule.runOnIdle {
assertThat(state.firstVisibleItemIndex).isEqualTo(1)
}
rule.onNodeWithTag(LazyListTag).performTouchInput { swipeRight() }
rule.runOnIdle {
assertThat(state.firstVisibleItemIndex).isEqualTo(0)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Test
fun topAppBar_pinned_allowHorizontalScroll() {
lateinit var state: LazyListState
rule.setMaterialContent(lightColorScheme()) {
state = rememberLazyListState()
MultiPageContent(
TopAppBarDefaults.pinnedScrollBehavior(),
state
)
}
rule.onNodeWithTag(LazyListTag).performTouchInput { swipeLeft() }
rule.runOnIdle {
assertThat(state.firstVisibleItemIndex).isEqualTo(1)
}
rule.onNodeWithTag(LazyListTag).performTouchInput { swipeRight() }
rule.runOnIdle {
assertThat(state.firstVisibleItemIndex).isEqualTo(0)
}
}
@Test
fun topAppBar_smallPinnedDraggedAppBar() {
rule.setMaterialContentForSizeAssertions {
SmallTopAppBar(
modifier = Modifier.testTag(TopAppBarTestTag),
title = {
Text("Title")
},
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
)
}
rule.onNodeWithTag(TopAppBarTestTag)
.assertHeightIsEqualTo(TopAppBarSmallTokens.ContainerHeight)
// Drag the app bar up half its height.
rule.onNodeWithTag(TopAppBarTestTag).performTouchInput {
down(Offset(x = 0f, y = height / 2f))
moveTo(Offset(x = 0f, y = 0f))
}
rule.waitForIdle()
// Check that the app bar did not collapse.
rule.onNodeWithTag(TopAppBarTestTag)
.assertHeightIsEqualTo(TopAppBarSmallTokens.ContainerHeight)
}
@Test
fun topAppBar_mediumDraggedAppBar() {
rule.setMaterialContentForSizeAssertions {
MediumTopAppBar(
modifier = Modifier.testTag(TopAppBarTestTag),
title = {
Text("Title")
},
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
)
}
rule.onNodeWithTag(TopAppBarTestTag)
.assertHeightIsEqualTo(TopAppBarMediumTokens.ContainerHeight)
// Drag up the app bar.
rule.onNodeWithTag(TopAppBarTestTag).performTouchInput {
down(Offset(x = 0f, y = height - 20f))
moveTo(Offset(x = 0f, y = 0f))
}
rule.waitForIdle()
// Check that the app bar collapsed to its small size constraints (i.e.
// TopAppBarSmallTokens.ContainerHeight).
rule.onNodeWithTag(TopAppBarTestTag)
.assertHeightIsEqualTo(TopAppBarSmallTokens.ContainerHeight)
}
@Test
fun topAppBar_dragSnapToCollapsed() {
rule.setMaterialContentForSizeAssertions {
LargeTopAppBar(
modifier = Modifier.testTag(TopAppBarTestTag),
title = {
Text("Title")
},
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
)
}
rule.onNodeWithTag(TopAppBarTestTag)
.assertHeightIsEqualTo(TopAppBarLargeTokens.ContainerHeight)
// Slightly drag up the app bar.
rule.onNodeWithTag(TopAppBarTestTag).performTouchInput {
down(Offset(x = 0f, y = height - 20f))
moveTo(Offset(x = 0f, y = height - 40f))
up()
}
rule.waitForIdle()
// Check that the app bar returned to its expanded size (i.e. fully expanded).
rule.onNodeWithTag(TopAppBarTestTag)
.assertHeightIsEqualTo(TopAppBarLargeTokens.ContainerHeight)
// Drag up the app bar to the point it should continue to collapse after.
rule.onNodeWithTag(TopAppBarTestTag).performTouchInput {
down(Offset(x = 0f, y = height - 20f))
moveTo(Offset(x = 0f, y = 40f))
up()
}
rule.waitForIdle()
// Check that the app bar collapsed to its small size constraints (i.e.
// TopAppBarSmallTokens.ContainerHeight).
rule.onNodeWithTag(TopAppBarTestTag)
.assertHeightIsEqualTo(TopAppBarSmallTokens.ContainerHeight)
}
@Test
fun state_restoresTopAppBarState() {
val restorationTester = StateRestorationTester(rule)
var topAppBarState: TopAppBarState? = null
restorationTester.setContent {
topAppBarState = rememberTopAppBarState()
}
rule.runOnIdle {
topAppBarState!!.heightOffsetLimit = -350f
topAppBarState!!.heightOffset = -300f
topAppBarState!!.contentOffset = -550f
}
topAppBarState = null
restorationTester.emulateSavedInstanceStateRestore()
rule.runOnIdle {
assertThat(topAppBarState!!.heightOffsetLimit).isEqualTo(-350f)
assertThat(topAppBarState!!.heightOffset).isEqualTo(-300f)
assertThat(topAppBarState!!.contentOffset).isEqualTo(-550f)
}
}
@Test
fun bottomAppBarWithFAB_heightIsFromSpec() {
rule
.setMaterialContentForSizeAssertions {
BottomAppBar(
actions = {},
floatingActionButton = {
FloatingActionButton(
onClick = { /* do something */ },
containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
) {
Icon(Icons.Filled.Add, "Localized description")
}
})
}
.assertHeightIsEqualTo(BottomAppBarTokens.ContainerHeight)
.assertWidthIsEqualTo(rule.rootWidth())
}
@Test
fun bottomAppBarWithFAB_respectsWindowInsets() {
rule
.setMaterialContentForSizeAssertions {
BottomAppBar(
actions = {},
windowInsets = WindowInsets(10.dp, 10.dp, 10.dp, 10.dp),
floatingActionButton = {
FloatingActionButton(
onClick = { /* do something */ },
containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
) {
Icon(Icons.Filled.Add, "Localized description")
}
})
}
.assertHeightIsEqualTo(BottomAppBarTokens.ContainerHeight + 20.dp)
.assertWidthIsEqualTo(rule.rootWidth())
}
@Test
fun bottomAppBar_widthExpandsToScreen() {
rule
.setMaterialContentForSizeAssertions {
BottomAppBar {}
}
.assertHeightIsEqualTo(BottomAppBarTokens.ContainerHeight)
.assertWidthIsEqualTo(rule.rootWidth())
}
@Test
fun bottomAppBar_default_positioning() {
rule.setMaterialContent(lightColorScheme()) {
BottomAppBar(Modifier.testTag("bar")) {
FakeIcon(Modifier.testTag("icon"))
}
}
val appBarBounds = rule.onNodeWithTag("bar").getUnclippedBoundsInRoot()
val appBarBottomEdgeY = appBarBounds.top + appBarBounds.height
val defaultPadding = BottomAppBarDefaults.ContentPadding
rule.onNodeWithTag("icon")
// Child icon should be 4.dp from the start
.assertLeftPositionInRootIsEqualTo(AppBarStartAndEndPadding)
// Child icon should be 10.dp from the top
.assertTopPositionInRootIsEqualTo(
defaultPadding.calculateTopPadding() +
(appBarBottomEdgeY - defaultPadding.calculateTopPadding() - FakeIconSize) / 2
)
}
@Test
fun bottomAppBar_default_positioning_respectsContentPadding() {
val topPadding = 5.dp
rule.setMaterialContent(lightColorScheme()) {
BottomAppBar(
Modifier.testTag("bar"),
contentPadding = PaddingValues(top = topPadding, start = 3.dp)
) {
FakeIcon(Modifier.testTag("icon"))
}
}
val appBarBounds = rule.onNodeWithTag("bar").getUnclippedBoundsInRoot()
val appBarBottomEdgeY = appBarBounds.top + appBarBounds.height
rule.onNodeWithTag("icon")
// Child icon should be 4.dp from the start
.assertLeftPositionInRootIsEqualTo(3.dp)
// Child icon should be 10.dp from the top
.assertTopPositionInRootIsEqualTo(
(appBarBottomEdgeY - topPadding - FakeIconSize) / 2 + 5.dp
)
}
@Test
fun bottomAppBarWithFAB_default_positioning() {
rule.setMaterialContent(lightColorScheme()) {
BottomAppBar(
actions = {},
Modifier.testTag("bar"),
floatingActionButton = {
FloatingActionButton(
onClick = { /* do something */ },
modifier = Modifier.testTag("FAB"),
containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
) {
Icon(Icons.Filled.Add, "Localized description")
}
})
}
val appBarBounds = rule.onNodeWithTag("bar").getUnclippedBoundsInRoot()
val fabBounds = rule.onNodeWithTag("FAB").getUnclippedBoundsInRoot()
rule.onNodeWithTag("FAB")
// FAB should be 16.dp from the end
.assertLeftPositionInRootIsEqualTo(appBarBounds.width - 16.dp - fabBounds.width)
// FAB should be 12.dp from the top
.assertTopPositionInRootIsEqualTo(12.dp)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun MultiPageContent(scrollBehavior: TopAppBarScrollBehavior, state: LazyListState) {
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
SmallTopAppBar(
modifier = Modifier.testTag(TopAppBarTestTag),
title = { Text(text = "Title") },
scrollBehavior = scrollBehavior,
)
}
) { contentPadding ->
LazyRow(
Modifier
.fillMaxSize()
.testTag(LazyListTag), state
) {
items(2) { page ->
LazyColumn(
modifier = Modifier.fillParentMaxSize(),
contentPadding = contentPadding
) {
items(50) {
Text(
modifier = Modifier.fillParentMaxWidth(),
text = "Item #$page x $it"
)
}
}
}
}
}
}
/**
* Checks the app bar's components positioning when it's a [SmallTopAppBar], a
* [CenterAlignedTopAppBar], or a larger app bar that is scrolled up and collapsed into a small
* configuration and there is no navigation icon.
*/
private fun assertSmallPositioningWithoutNavigation(isCenteredTitle: Boolean = false) {
val appBarBounds = rule.onNodeWithTag(TopAppBarTestTag).getUnclippedBoundsInRoot()
val titleBounds = rule.onNodeWithTag(TitleTestTag).getUnclippedBoundsInRoot()
val titleNode = rule.onNodeWithTag(TitleTestTag)
// Title should be vertically centered
titleNode.assertTopPositionInRootIsEqualTo((appBarBounds.height - titleBounds.height) / 2)
if (isCenteredTitle) {
// Title should be horizontally centered
titleNode.assertLeftPositionInRootIsEqualTo(
(appBarBounds.width - titleBounds.width) / 2
)
} else {
// Title should now be placed 16.dp from the start, as there is no navigation icon
// 4.dp padding for the whole app bar + 12.dp inset
titleNode.assertLeftPositionInRootIsEqualTo(4.dp + 12.dp)
}
rule.onNodeWithTag(ActionsTestTag)
// Action should still be placed at the end
.assertLeftPositionInRootIsEqualTo(expectedActionPosition(appBarBounds.width))
}
/**
* Checks the app bar's components positioning when it's a [SmallTopAppBar] or a
* [CenterAlignedTopAppBar].
*/
private fun assertSmallDefaultPositioning(isCenteredTitle: Boolean = false) {
val appBarBounds = rule.onNodeWithTag(TopAppBarTestTag).getUnclippedBoundsInRoot()
val titleBounds = rule.onNodeWithTag(TitleTestTag).getUnclippedBoundsInRoot()
val appBarBottomEdgeY = appBarBounds.top + appBarBounds.height
rule.onNodeWithTag(NavigationIconTestTag)
// Navigation icon should be 4.dp from the start
.assertLeftPositionInRootIsEqualTo(AppBarStartAndEndPadding)
// Navigation icon should be centered within the height of the app bar.
.assertTopPositionInRootIsEqualTo(
appBarBottomEdgeY - AppBarTopAndBottomPadding - FakeIconSize
)
val titleNode = rule.onNodeWithTag(TitleTestTag)
// Title should be vertically centered
titleNode.assertTopPositionInRootIsEqualTo((appBarBounds.height - titleBounds.height) / 2)
if (isCenteredTitle) {
// Title should be horizontally centered
titleNode.assertLeftPositionInRootIsEqualTo(
(appBarBounds.width - titleBounds.width) / 2
)
} else {
// Title should be 56.dp from the start
// 4.dp padding for the whole app bar + 48.dp icon size + 4.dp title padding.
titleNode.assertLeftPositionInRootIsEqualTo(4.dp + FakeIconSize + 4.dp)
}
rule.onNodeWithTag(ActionsTestTag)
// Action should be placed at the end
.assertLeftPositionInRootIsEqualTo(expectedActionPosition(appBarBounds.width))
// Action should be 8.dp from the top
.assertTopPositionInRootIsEqualTo(
appBarBottomEdgeY - AppBarTopAndBottomPadding - FakeIconSize
)
}
/**
* Checks the app bar's components positioning when it's a [MediumTopAppBar] or a
* [LargeTopAppBar].
*/
private fun assertMediumOrLargeDefaultPositioning(
expectedAppBarHeight: Dp,
bottomTextPadding: Dp
) {
val appBarBounds = rule.onNodeWithTag(TopAppBarTestTag).getUnclippedBoundsInRoot()
appBarBounds.height.assertIsEqualTo(expectedAppBarHeight, "top app bar height")
// Expecting the title composable to be reused for the top and bottom rows of the top app
// bar, so obtaining the node with the title tag should return two nodes, one for each row.
val allTitleNodes = rule.onAllNodesWithTag(TitleTestTag, true)
allTitleNodes.assertCountEquals(2)
val topTitleNode = allTitleNodes.onFirst()
val bottomTitleNode = allTitleNodes.onLast()
val topTitleBounds = topTitleNode.getUnclippedBoundsInRoot()
val bottomTitleBounds = bottomTitleNode.getUnclippedBoundsInRoot()
val topAppBarBottomEdgeY = appBarBounds.top + TopAppBarSmallTokens.ContainerHeight
val bottomAppBarBottomEdgeY = appBarBounds.top + appBarBounds.height
rule.onNodeWithTag(NavigationIconTestTag)
// Navigation icon should be 4.dp from the start
.assertLeftPositionInRootIsEqualTo(AppBarStartAndEndPadding)
// Navigation icon should be centered within the height of the top part of the app bar.
.assertTopPositionInRootIsEqualTo(
topAppBarBottomEdgeY - AppBarTopAndBottomPadding - FakeIconSize
)
rule.onNodeWithTag(ActionsTestTag)
// Action should be placed at the end
.assertLeftPositionInRootIsEqualTo(expectedActionPosition(appBarBounds.width))
// Action should be 8.dp from the top
.assertTopPositionInRootIsEqualTo(
topAppBarBottomEdgeY - AppBarTopAndBottomPadding - FakeIconSize
)
topTitleNode
// Top title should be 56.dp from the start
// 4.dp padding for the whole app bar + 48.dp icon size + 4.dp title padding.
.assertLeftPositionInRootIsEqualTo(4.dp + FakeIconSize + 4.dp)
// Title should be vertically centered in the top part, which has a height of a small
// app bar.
.assertTopPositionInRootIsEqualTo((topAppBarBottomEdgeY - topTitleBounds.height) / 2)
bottomTitleNode
// Bottom title should be 16.dp from the start.
.assertLeftPositionInRootIsEqualTo(16.dp)
// Check if the bottom text baseline is at the expected distance from the bottom of the
// app bar.
val bottomTextBaselineY = bottomTitleBounds.top + bottomTitleNode.getLastBaselinePosition()
(bottomAppBarBottomEdgeY - bottomTextBaselineY).assertIsEqualTo(
bottomTextPadding,
"text baseline distance from the bottom"
)
}
/**
* Checks that changing values at a [MediumTopAppBar] or a [LargeTopAppBar] scroll behavior
* affects the height of the app bar.
*
* This check partially and fully collapses the app bar to test its height.
*
* @param appBarMaxHeight the max height of the app bar [content]
* @param appBarMinHeight the min height of the app bar [content]
* @param content a Composable that adds a MediumTopAppBar or a LargeTopAppBar
*/
@OptIn(ExperimentalMaterial3Api::class)
private fun assertMediumOrLargeScrolledHeight(
appBarMaxHeight: Dp,
appBarMinHeight: Dp,
windowInsets: WindowInsets,
content: @Composable (TopAppBarScrollBehavior?) -> Unit
) {
val (topInset, bottomInset) = with(rule.density) {
windowInsets.getTop(this).toDp() to windowInsets.getBottom(this).toDp()
}
val fullyCollapsedOffsetDp = appBarMaxHeight - appBarMinHeight
val partiallyCollapsedOffsetDp = fullyCollapsedOffsetDp / 3
var partiallyCollapsedHeightOffsetPx = 0f
var fullyCollapsedHeightOffsetPx = 0f
lateinit var scrollBehavior: TopAppBarScrollBehavior
rule.setMaterialContent(lightColorScheme()) {
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
with(LocalDensity.current) {
partiallyCollapsedHeightOffsetPx = partiallyCollapsedOffsetDp.toPx()
fullyCollapsedHeightOffsetPx = fullyCollapsedOffsetDp.toPx()
}
content(scrollBehavior)
}
// Simulate a partially collapsed app bar.
rule.runOnIdle {
scrollBehavior.state.heightOffset = -partiallyCollapsedHeightOffsetPx
scrollBehavior.state.contentOffset = -partiallyCollapsedHeightOffsetPx
}
rule.waitForIdle()
rule.onNodeWithTag(TopAppBarTestTag)
.assertHeightIsEqualTo(
appBarMaxHeight - partiallyCollapsedOffsetDp + topInset + bottomInset
)
// Simulate a fully collapsed app bar.
rule.runOnIdle {
scrollBehavior.state.heightOffset = -fullyCollapsedHeightOffsetPx
// Simulate additional content scroll beyond the max offset scroll.
scrollBehavior.state.contentOffset =
-fullyCollapsedHeightOffsetPx - partiallyCollapsedHeightOffsetPx
}
rule.waitForIdle()
// Check that the app bar collapsed to its min height.
rule.onNodeWithTag(TopAppBarTestTag).assertHeightIsEqualTo(
appBarMinHeight + topInset + bottomInset
)
}
/**
* Checks that changing values at a [MediumTopAppBar] or a [LargeTopAppBar] scroll behavior
* affects the container color and the title's content color of the app bar.
*
* This check partially and fully collapses the app bar to test its colors.
*
* @param appBarMaxHeight the max height of the app bar [content]
* @param appBarMinHeight the min height of the app bar [content]
* @param content a Composable that adds a MediumTopAppBar or a LargeTopAppBar
*/
@OptIn(ExperimentalMaterial3Api::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
private fun assertMediumOrLargeScrolledColors(
appBarMaxHeight: Dp,
appBarMinHeight: Dp,
content: @Composable (TopAppBarScrollBehavior?) -> Unit
) {
val fullyCollapsedOffsetDp = appBarMaxHeight - appBarMinHeight
val oneThirdCollapsedOffsetDp = fullyCollapsedOffsetDp / 3
var fullyCollapsedHeightOffsetPx = 0f
var oneThirdCollapsedHeightOffsetPx = 0f
var fullyCollapsedContainerColor: Color = Color.Unspecified
var oneThirdCollapsedContainerColor: Color = Color.Unspecified
var titleContentColor: Color = Color.Unspecified
lateinit var scrollBehavior: TopAppBarScrollBehavior
rule.setMaterialContent(lightColorScheme()) {
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
// Using the mediumTopAppBarColors for both Medium and Large top app bars, as the
// current content color settings are the same.
oneThirdCollapsedContainerColor =
TopAppBarDefaults.mediumTopAppBarColors()
.containerColor(colorTransitionFraction = 1 / 3f)
fullyCollapsedContainerColor =
TopAppBarDefaults.mediumTopAppBarColors()
.containerColor(colorTransitionFraction = 1f)
// Resolve the title's content color. The default implementation returns the same color
// regardless of the fraction, and the color is applied later with alpha.
titleContentColor =
TopAppBarDefaults.mediumTopAppBarColors().titleContentColor
with(LocalDensity.current) {
oneThirdCollapsedHeightOffsetPx = oneThirdCollapsedOffsetDp.toPx()
fullyCollapsedHeightOffsetPx = fullyCollapsedOffsetDp.toPx()
}
content(scrollBehavior)
}
// Expecting the title composable to be reused for the top and bottom rows of the top app
// bar, so obtaining the node with the title tag should return two nodes, one for each row.
val allTitleNodes = rule.onAllNodesWithTag(TitleTestTag, true)
allTitleNodes.assertCountEquals(2)
val topTitleNode = allTitleNodes.onFirst()
val bottomTitleNode = allTitleNodes.onLast()
// Simulate 1/3 collapsed content.
rule.runOnIdle {
scrollBehavior.state.heightOffset = -oneThirdCollapsedHeightOffsetPx
scrollBehavior.state.contentOffset = -oneThirdCollapsedHeightOffsetPx
}
rule.waitForIdle()
rule.onNodeWithTag(TopAppBarTestTag).captureToImage()
.assertContainsColor(oneThirdCollapsedContainerColor)
// Both top and bottom titles should be visible. The top should have the title text color
// with ~33.333% alpha, and the bottom with ~66.666% alpha.
topTitleNode.captureToImage()
.assertContainsColor(
titleContentColor.copy(alpha = 1 / 3f)
.compositeOver(oneThirdCollapsedContainerColor)
)
bottomTitleNode.captureToImage()
.assertContainsColor(
titleContentColor.copy(alpha = 2 / 3f)
.compositeOver(oneThirdCollapsedContainerColor)
)
// Simulate fully collapsed content.
rule.runOnIdle {
scrollBehavior.state.heightOffset = -fullyCollapsedHeightOffsetPx
scrollBehavior.state.contentOffset = -fullyCollapsedHeightOffsetPx
}
rule.waitForIdle()
rule.onNodeWithTag(TopAppBarTestTag).captureToImage()
.assertContainsColor(fullyCollapsedContainerColor)
// Only the top title should be visible in the collapsed form.
topTitleNode.captureToImage().assertContainsColor(titleContentColor)
bottomTitleNode.assertIsNotDisplayed()
}
/**
* Checks that changing values at a [MediumTopAppBar] or a [LargeTopAppBar] scroll behavior
* affects the title's semantics.
*
* This check partially and fully collapses the app bar to test the semantics.
*
* @param appBarMaxHeight the max height of the app bar [content]
* @param appBarMinHeight the min height of the app bar [content]
* @param content a Composable that adds a MediumTopAppBar or a LargeTopAppBar
*/
@OptIn(ExperimentalMaterial3Api::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
private fun assertMediumOrLargeScrolledSemantics(
appBarMaxHeight: Dp,
appBarMinHeight: Dp,
content: @Composable (TopAppBarScrollBehavior?) -> Unit
) {
val fullyCollapsedOffsetDp = appBarMaxHeight - appBarMinHeight
val oneThirdCollapsedOffsetDp = fullyCollapsedOffsetDp / 3
var fullyCollapsedHeightOffsetPx = 0f
var oneThirdCollapsedHeightOffsetPx = 0f
lateinit var scrollBehavior: TopAppBarScrollBehavior
rule.setMaterialContent(lightColorScheme()) {
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
with(LocalDensity.current) {
oneThirdCollapsedHeightOffsetPx = oneThirdCollapsedOffsetDp.toPx()
fullyCollapsedHeightOffsetPx = fullyCollapsedOffsetDp.toPx()
}
content(scrollBehavior)
}
// Asserting that only one semantic title node is returned after the clearAndSetSemantics is
// applied to the merged tree according to the alpha values of the titles.
assertSingleTitleSemanticNode()
// Simulate 1/3 collapsed content.
rule.runOnIdle {
scrollBehavior.state.heightOffset = -oneThirdCollapsedHeightOffsetPx
scrollBehavior.state.contentOffset = -oneThirdCollapsedHeightOffsetPx
}
rule.waitForIdle()
// Assert that only one semantic title node is available while scrolling the app bar.
assertSingleTitleSemanticNode()
// Simulate fully collapsed content.
rule.runOnIdle {
scrollBehavior.state.heightOffset = -fullyCollapsedHeightOffsetPx
scrollBehavior.state.contentOffset = -fullyCollapsedHeightOffsetPx
}
rule.waitForIdle()
// Assert that only one semantic title node is available.
assertSingleTitleSemanticNode()
}
/**
* Asserts that only one semantic node exists at app bar title when the tree is merged.
*/
private fun assertSingleTitleSemanticNode() {
val unmergedTitleNodes = rule.onAllNodesWithTag(TitleTestTag, useUnmergedTree = true)
unmergedTitleNodes.assertCountEquals(2)
val mergedTitleNodes = rule.onAllNodesWithTag(TitleTestTag, useUnmergedTree = false)
mergedTitleNodes.assertCountEquals(1)
}
/**
* An [IconButton] with an [Icon] inside for testing positions.
*
* An [IconButton] is defaulted to be 48X48dp, while its child [Icon] is defaulted to 24x24dp.
*/
private val FakeIcon = @Composable { modifier: Modifier ->
IconButton(
onClick = { /* doSomething() */ },
modifier = modifier.semantics(mergeDescendants = true) {}
) {
Icon(ColorPainter(Color.Red), null)
}
}
private fun expectedActionPosition(appBarWidth: Dp): Dp =
appBarWidth - AppBarStartAndEndPadding - FakeIconSize
private val FakeIconSize = 48.dp
private val AppBarStartAndEndPadding = 4.dp
private val AppBarTopAndBottomPadding =
(TopAppBarSmallTokens.ContainerHeight - FakeIconSize) / 2
private val LazyListTag = "lazyList"
private val TopAppBarTestTag = "bar"
private val NavigationIconTestTag = "navigationIcon"
private val TitleTestTag = "title"
private val ActionsTestTag = "actions"
}