| /* |
| * 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.compose.material |
| |
| import androidx.compose.runtime.Composable |
| import androidx.compose.runtime.getValue |
| import androidx.compose.runtime.setValue |
| import androidx.compose.runtime.state |
| import androidx.test.filters.LargeTest |
| import androidx.ui.core.Modifier |
| import androidx.ui.core.testTag |
| import androidx.compose.foundation.Box |
| import androidx.compose.foundation.Icon |
| import androidx.compose.foundation.Text |
| import androidx.compose.foundation.background |
| import androidx.compose.ui.graphics.Color |
| import androidx.compose.foundation.layout.fillMaxWidth |
| import androidx.compose.foundation.layout.preferredHeight |
| import androidx.compose.material.icons.Icons |
| import androidx.compose.material.icons.filled.Favorite |
| import androidx.compose.material.samples.ScrollingTextTabs |
| import androidx.compose.material.samples.TextTabs |
| import androidx.ui.test.assertCountEquals |
| import androidx.ui.test.assertHeightIsEqualTo |
| import androidx.ui.test.assertIsEqualTo |
| import androidx.ui.test.assertIsSelected |
| import androidx.ui.test.assertIsNotSelected |
| import androidx.ui.test.assertPositionInRootIsEqualTo |
| import androidx.ui.test.createComposeRule |
| import androidx.ui.test.performClick |
| import androidx.ui.test.onAllNodes |
| import androidx.ui.test.onNodeWithTag |
| import androidx.ui.test.getBoundsInRoot |
| import androidx.ui.test.isInMutuallyExclusiveGroup |
| import androidx.compose.ui.unit.dp |
| import androidx.compose.ui.unit.height |
| import androidx.compose.ui.unit.width |
| import org.junit.Rule |
| import org.junit.Test |
| import org.junit.runner.RunWith |
| import org.junit.runners.JUnit4 |
| |
| @LargeTest |
| @RunWith(JUnit4::class) |
| class TabTest { |
| |
| private val ExpectedSmallTabHeight = 48.dp |
| private val ExpectedLargeTabHeight = 72.dp |
| |
| private val icon = Icons.Filled.Favorite |
| |
| @get:Rule |
| val composeTestRule = createComposeRule(disableTransitions = true) |
| |
| @Test |
| fun textTab_height() { |
| composeTestRule |
| .setMaterialContentForSizeAssertions { |
| Tab(text = { Text("Text") }, selected = true, onSelected = {}) |
| } |
| .assertHeightIsEqualTo(ExpectedSmallTabHeight) |
| } |
| |
| @Test |
| fun iconTab_height() { |
| composeTestRule |
| .setMaterialContentForSizeAssertions { |
| Tab(icon = { Icon(icon) }, selected = true, onSelected = {}) |
| } |
| .assertHeightIsEqualTo(ExpectedSmallTabHeight) |
| } |
| |
| @Test |
| fun textAndIconTab_height() { |
| composeTestRule |
| .setMaterialContentForSizeAssertions { |
| Surface { |
| Tab( |
| text = { Text("Text and Icon") }, |
| icon = { Icon(icon) }, |
| selected = true, |
| onSelected = {} |
| ) |
| } |
| } |
| .assertHeightIsEqualTo(ExpectedLargeTabHeight) |
| } |
| |
| @Test |
| fun fixedTabRow_indicatorPosition() { |
| val indicatorHeight = 1.dp |
| |
| composeTestRule.setMaterialContent { |
| var state by state { 0 } |
| val titles = listOf("TAB 1", "TAB 2") |
| |
| val indicatorContainer = @Composable { tabPositions: List<TabRow.TabPosition> -> |
| TabRow.IndicatorContainer(tabPositions, state) { |
| Box( |
| Modifier |
| .fillMaxWidth() |
| .preferredHeight(indicatorHeight) |
| .background(color = Color.Red) |
| .testTag("indicator") |
| ) |
| } |
| } |
| |
| Box(Modifier.testTag("tabRow")) { |
| TabRow( |
| items = titles, |
| selectedIndex = state, |
| indicatorContainer = indicatorContainer |
| ) { index, text -> |
| Tab( |
| text = { Text(text) }, |
| selected = state == index, |
| onSelected = { state = index } |
| ) |
| } |
| } |
| } |
| |
| val tabRowBounds = onNodeWithTag("tabRow").getBoundsInRoot() |
| |
| onNodeWithTag("indicator") |
| .assertPositionInRootIsEqualTo( |
| expectedLeft = 0.dp, |
| expectedTop = tabRowBounds.height - indicatorHeight |
| ) |
| |
| // Click the second tab |
| onAllNodes(isInMutuallyExclusiveGroup())[1].performClick() |
| |
| // Indicator should now be placed in the bottom left of the second tab, so its x coordinate |
| // should be in the middle of the TabRow |
| onNodeWithTag("indicator") |
| .assertPositionInRootIsEqualTo( |
| expectedLeft = (tabRowBounds.width / 2), |
| expectedTop = tabRowBounds.height - indicatorHeight |
| ) |
| } |
| |
| @Test |
| fun singleLineTab_textBaseline() { |
| composeTestRule.setMaterialContent { |
| var state by state { 0 } |
| val titles = listOf("TAB") |
| |
| Box { |
| TabRow( |
| modifier = Modifier.testTag("tabRow"), |
| items = titles, |
| selectedIndex = state |
| ) { index, text -> |
| Tab( |
| text = { |
| Text(text, Modifier.testTag("text")) |
| }, |
| selected = state == index, |
| onSelected = { state = index } |
| ) |
| } |
| } |
| } |
| |
| val expectedBaseline = 18.dp |
| val indicatorHeight = 2.dp |
| val expectedBaselineDistance = expectedBaseline + indicatorHeight |
| |
| val tabRowBounds = onNodeWithTag("tabRow").getBoundsInRoot() |
| val textBounds = onNodeWithTag("text").getBoundsInRoot() |
| val textBaselinePos = onNodeWithTag("text").getLastBaselinePosition() |
| |
| val baselinePositionY = textBounds.top + textBaselinePos |
| val expectedPositionY = tabRowBounds.height - expectedBaselineDistance |
| baselinePositionY.assertIsEqualTo(expectedPositionY, "baseline y-position") |
| } |
| |
| @Test |
| fun singleLineTab_withIcon_textBaseline() { |
| composeTestRule.setMaterialContent { |
| var state by state { 0 } |
| val titles = listOf("TAB") |
| |
| Box { |
| TabRow( |
| modifier = Modifier.testTag("tabRow"), |
| items = titles, |
| selectedIndex = state |
| ) { index, text -> |
| Tab( |
| text = { |
| Text(text, Modifier.testTag("text")) |
| }, |
| icon = { Icon(Icons.Filled.Favorite) }, |
| selected = state == index, |
| onSelected = { state = index } |
| ) |
| } |
| } |
| } |
| |
| val expectedBaseline = 14.dp |
| val indicatorHeight = 2.dp |
| val expectedBaselineDistance = expectedBaseline + indicatorHeight |
| |
| val tabRowBounds = onNodeWithTag("tabRow").getBoundsInRoot() |
| val textBounds = onNodeWithTag("text").getBoundsInRoot() |
| val textBaselinePos = onNodeWithTag("text").getLastBaselinePosition() |
| |
| val baselinePositionY = textBounds.top + textBaselinePos |
| val expectedPositionY = tabRowBounds.height - expectedBaselineDistance |
| baselinePositionY.assertIsEqualTo(expectedPositionY, "baseline y-position") |
| } |
| |
| @Test |
| fun twoLineTab_textBaseline() { |
| composeTestRule.setMaterialContent { |
| var state by state { 0 } |
| val titles = listOf("Two line \n text") |
| |
| Box { |
| TabRow( |
| modifier = Modifier.testTag("tabRow"), |
| items = titles, |
| selectedIndex = state |
| ) { index, text -> |
| Tab( |
| text = { |
| Text(text, Modifier.testTag("text"), maxLines = 2) |
| }, |
| selected = state == index, |
| onSelected = { state = index } |
| ) |
| } |
| } |
| } |
| |
| val expectedBaseline = 10.dp |
| val indicatorHeight = 2.dp |
| |
| val tabRowBounds = onNodeWithTag("tabRow").getBoundsInRoot() |
| val textBounds = onNodeWithTag("text").getBoundsInRoot() |
| val textBaselinePos = onNodeWithTag("text").getLastBaselinePosition() |
| |
| val expectedBaselineDistance = expectedBaseline + indicatorHeight |
| |
| val baselinePositionY = textBounds.top + textBaselinePos |
| val expectedPositionY = (tabRowBounds.height - expectedBaselineDistance) |
| baselinePositionY.assertIsEqualTo(expectedPositionY, "baseline y-position") |
| } |
| |
| @Test |
| fun scrollableTabRow_indicatorPosition() { |
| val indicatorHeight = 1.dp |
| val scrollableTabRowOffset = 52.dp |
| val minimumTabWidth = 90.dp |
| |
| composeTestRule.setMaterialContent { |
| var state by state { 0 } |
| val titles = listOf("TAB 1", "TAB 2") |
| |
| val indicatorContainer = @Composable { tabPositions: List<TabRow.TabPosition> -> |
| TabRow.IndicatorContainer(tabPositions, state) { |
| Box( |
| Modifier |
| .fillMaxWidth() |
| .preferredHeight(indicatorHeight) |
| .background(color = Color.Red) |
| .testTag("indicator") |
| ) |
| } |
| } |
| |
| Box { |
| TabRow( |
| modifier = Modifier.testTag("tabRow"), |
| items = titles, |
| scrollable = true, |
| selectedIndex = state, |
| indicatorContainer = indicatorContainer |
| ) { index, text -> |
| Tab( |
| text = { Text(text) }, |
| selected = state == index, |
| onSelected = { state = index } |
| ) |
| } |
| } |
| } |
| |
| val tabRowBounds = onNodeWithTag("tabRow").getBoundsInRoot() |
| |
| // Indicator should be placed in the bottom left of the first tab |
| onNodeWithTag("indicator") |
| .assertPositionInRootIsEqualTo( |
| // Tabs in a scrollable tab row are offset 52.dp from each end |
| expectedLeft = scrollableTabRowOffset, |
| expectedTop = tabRowBounds.height - indicatorHeight |
| ) |
| |
| // Click the second tab |
| onAllNodes(isInMutuallyExclusiveGroup())[1].performClick() |
| |
| // Indicator should now be placed in the bottom left of the second tab, so its x coordinate |
| // should be in the middle of the TabRow |
| onNodeWithTag("indicator") |
| .assertPositionInRootIsEqualTo( |
| expectedLeft = scrollableTabRowOffset + minimumTabWidth, |
| expectedTop = tabRowBounds.height - indicatorHeight |
| ) |
| } |
| |
| @Test |
| fun fixedTabRow_initialTabSelected() { |
| composeTestRule |
| .setMaterialContent { |
| TextTabs() |
| } |
| |
| // Only the first tab should be selected |
| onAllNodes(isInMutuallyExclusiveGroup()) |
| .assertCountEquals(3) |
| .apply { |
| get(0).assertIsSelected() |
| get(1).assertIsNotSelected() |
| get(2).assertIsNotSelected() |
| } |
| } |
| |
| @Test |
| fun fixedTabRow_selectNewTab() { |
| composeTestRule |
| .setMaterialContent { |
| TextTabs() |
| } |
| |
| // Only the first tab should be selected |
| onAllNodes(isInMutuallyExclusiveGroup()) |
| .assertCountEquals(3) |
| .apply { |
| get(0).assertIsSelected() |
| get(1).assertIsNotSelected() |
| get(2).assertIsNotSelected() |
| } |
| |
| // Click the last tab |
| onAllNodes(isInMutuallyExclusiveGroup())[2].performClick() |
| |
| // Now only the last tab should be selected |
| onAllNodes(isInMutuallyExclusiveGroup()) |
| .assertCountEquals(3) |
| .apply { |
| get(0).assertIsNotSelected() |
| get(1).assertIsNotSelected() |
| get(2).assertIsSelected() |
| } |
| } |
| |
| @Test |
| fun scrollableTabRow_initialTabSelected() { |
| composeTestRule |
| .setMaterialContent { |
| ScrollingTextTabs() |
| } |
| |
| // Only the first tab should be selected |
| onAllNodes(isInMutuallyExclusiveGroup()) |
| .assertCountEquals(10) |
| .apply { |
| get(0).assertIsSelected() |
| (1..9).forEach { |
| get(it).assertIsNotSelected() |
| } |
| } |
| } |
| |
| @Test |
| fun scrollableTabRow_selectNewTab() { |
| composeTestRule |
| .setMaterialContent { |
| ScrollingTextTabs() |
| } |
| |
| // Only the first tab should be selected |
| onAllNodes(isInMutuallyExclusiveGroup()) |
| .assertCountEquals(10) |
| .apply { |
| get(0).assertIsSelected() |
| (1..9).forEach { |
| get(it).assertIsNotSelected() |
| } |
| } |
| |
| // Click the second tab |
| onAllNodes(isInMutuallyExclusiveGroup())[1].performClick() |
| |
| // Now only the second tab should be selected |
| onAllNodes(isInMutuallyExclusiveGroup()) |
| .assertCountEquals(10) |
| .apply { |
| get(0).assertIsNotSelected() |
| get(1).assertIsSelected() |
| (2..9).forEach { |
| get(it).assertIsNotSelected() |
| } |
| } |
| } |
| } |