| /* |
| * 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.foundation.lazy.grid |
| |
| import androidx.compose.foundation.AutoTestFrameClock |
| import androidx.compose.foundation.ExperimentalFoundationApi |
| import androidx.compose.foundation.gestures.scrollBy |
| import androidx.compose.foundation.layout.Arrangement |
| import androidx.compose.foundation.layout.Box |
| import androidx.compose.foundation.layout.PaddingValues |
| import androidx.compose.foundation.layout.Spacer |
| import androidx.compose.foundation.layout.fillMaxWidth |
| import androidx.compose.foundation.layout.height |
| import androidx.compose.foundation.layout.requiredHeight |
| import androidx.compose.foundation.layout.requiredSize |
| import androidx.compose.foundation.layout.requiredWidth |
| import androidx.compose.foundation.layout.size |
| import androidx.compose.foundation.layout.width |
| import androidx.compose.foundation.lazy.GridCells |
| import androidx.compose.foundation.lazy.GridItemSpan |
| import androidx.compose.foundation.lazy.LazyGridState |
| import androidx.compose.foundation.lazy.LazyVerticalGrid |
| import androidx.compose.foundation.lazy.items |
| import androidx.compose.foundation.lazy.itemsIndexed |
| import androidx.compose.foundation.lazy.list.scrollBy |
| import androidx.compose.foundation.lazy.list.setContentWithTestViewConfiguration |
| import androidx.compose.foundation.lazy.rememberLazyGridState |
| import androidx.compose.runtime.CompositionLocalProvider |
| import androidx.compose.runtime.getValue |
| import androidx.compose.runtime.mutableStateOf |
| import androidx.compose.runtime.setValue |
| import androidx.compose.ui.Modifier |
| import androidx.compose.ui.composed |
| import androidx.compose.ui.layout.layout |
| import androidx.compose.ui.platform.LocalLayoutDirection |
| import androidx.compose.ui.platform.testTag |
| import androidx.compose.ui.semantics.SemanticsActions |
| import androidx.compose.ui.semantics.SemanticsProperties |
| import androidx.compose.ui.test.SemanticsMatcher |
| import androidx.compose.ui.test.SemanticsMatcher.Companion.keyIsDefined |
| import androidx.compose.ui.test.assert |
| import androidx.compose.ui.test.assertHeightIsAtLeast |
| import androidx.compose.ui.test.assertIsDisplayed |
| import androidx.compose.ui.test.assertIsNotDisplayed |
| import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo |
| import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo |
| import androidx.compose.ui.test.assertWidthIsAtLeast |
| import androidx.compose.ui.test.assertWidthIsEqualTo |
| import androidx.compose.ui.test.junit4.createComposeRule |
| import androidx.compose.ui.test.onNodeWithTag |
| import androidx.compose.ui.unit.LayoutDirection |
| import androidx.compose.ui.unit.dp |
| import androidx.test.ext.junit.runners.AndroidJUnit4 |
| import androidx.test.filters.MediumTest |
| import com.google.common.truth.Truth |
| import kotlinx.coroutines.runBlocking |
| import org.junit.Rule |
| import org.junit.Test |
| import org.junit.runner.RunWith |
| |
| @OptIn(ExperimentalFoundationApi::class) |
| @MediumTest |
| @RunWith(AndroidJUnit4::class) |
| class LazyGridTest { |
| private val LazyGridTag = "LazyGridTag" |
| |
| @get:Rule |
| val rule = createComposeRule() |
| |
| @Test |
| fun lazyGridShowsOneItem() { |
| val itemTestTag = "itemTestTag" |
| |
| rule.setContent { |
| LazyVerticalGrid( |
| cells = GridCells.Fixed(3) |
| ) { |
| item { |
| Spacer( |
| Modifier.size(10.dp).testTag(itemTestTag) |
| ) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag(itemTestTag) |
| .assertIsDisplayed() |
| } |
| |
| @Test |
| fun lazyGridShowsOneRow() { |
| val items = (1..5).map { it.toString() } |
| |
| rule.setContent { |
| LazyVerticalGrid( |
| cells = GridCells.Fixed(3), |
| modifier = Modifier.height(100.dp).width(300.dp) |
| ) { |
| items(items) { |
| Spacer(Modifier.height(101.dp).testTag(it)) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("1") |
| .assertIsDisplayed() |
| |
| rule.onNodeWithTag("2") |
| .assertIsDisplayed() |
| |
| rule.onNodeWithTag("3") |
| .assertIsDisplayed() |
| |
| rule.onNodeWithTag("4") |
| .assertDoesNotExist() |
| |
| rule.onNodeWithTag("5") |
| .assertDoesNotExist() |
| } |
| |
| @Test |
| fun lazyGridShowsSecondRowOnScroll() { |
| val items = (1..9).map { it.toString() } |
| |
| rule.setContentWithTestViewConfiguration { |
| LazyVerticalGrid( |
| cells = GridCells.Fixed(3), |
| modifier = Modifier.height(100.dp).testTag(LazyGridTag) |
| ) { |
| items(items) { |
| Spacer(Modifier.height(101.dp).testTag(it)) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag(LazyGridTag) |
| .scrollBy(y = 50.dp, density = rule.density) |
| |
| rule.onNodeWithTag("4") |
| .assertIsDisplayed() |
| |
| rule.onNodeWithTag("5") |
| .assertIsDisplayed() |
| |
| rule.onNodeWithTag("6") |
| .assertIsDisplayed() |
| |
| rule.onNodeWithTag("7") |
| .assertIsNotDisplayed() |
| |
| rule.onNodeWithTag("8") |
| .assertIsNotDisplayed() |
| |
| rule.onNodeWithTag("9") |
| .assertIsNotDisplayed() |
| } |
| |
| @Test |
| fun lazyGridScrollHidesFirstRow() { |
| val items = (1..9).map { it.toString() } |
| |
| rule.setContentWithTestViewConfiguration { |
| LazyVerticalGrid( |
| cells = GridCells.Fixed(3), |
| modifier = Modifier.height(200.dp).testTag(LazyGridTag) |
| ) { |
| items(items) { |
| Spacer(Modifier.height(101.dp).testTag(it)) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag(LazyGridTag) |
| .scrollBy(y = 103.dp, density = rule.density) |
| |
| rule.onNodeWithTag("1") |
| .assertIsNotDisplayed() |
| |
| rule.onNodeWithTag("2") |
| .assertIsNotDisplayed() |
| |
| rule.onNodeWithTag("3") |
| .assertDoesNotExist() |
| |
| rule.onNodeWithTag("4") |
| .assertIsDisplayed() |
| |
| rule.onNodeWithTag("5") |
| .assertIsDisplayed() |
| |
| rule.onNodeWithTag("6") |
| .assertIsDisplayed() |
| |
| rule.onNodeWithTag("7") |
| .assertIsDisplayed() |
| |
| rule.onNodeWithTag("8") |
| .assertIsDisplayed() |
| |
| rule.onNodeWithTag("9") |
| .assertIsDisplayed() |
| } |
| |
| @Test |
| fun adaptiveLazyGridFillsAllWidth() { |
| val items = (1..5).map { it.toString() } |
| |
| rule.setContent { |
| LazyVerticalGrid( |
| cells = GridCells.Adaptive(130.dp), |
| modifier = Modifier.height(100.dp).width(300.dp) |
| ) { |
| items(items) { |
| Spacer(Modifier.height(101.dp).testTag(it)) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("1") |
| .assertLeftPositionInRootIsEqualTo(0.dp) |
| |
| rule.onNodeWithTag("2") |
| .assertLeftPositionInRootIsEqualTo(150.dp) |
| |
| rule.onNodeWithTag("3") |
| .assertDoesNotExist() |
| |
| rule.onNodeWithTag("4") |
| .assertDoesNotExist() |
| |
| rule.onNodeWithTag("5") |
| .assertDoesNotExist() |
| } |
| |
| @Test |
| fun adaptiveLazyGridAtLeastOneColumn() { |
| val items = (1..3).map { it.toString() } |
| |
| rule.setContent { |
| LazyVerticalGrid( |
| cells = GridCells.Adaptive(301.dp), |
| modifier = Modifier.height(100.dp).width(300.dp) |
| ) { |
| items(items) { |
| Spacer(Modifier.height(101.dp).testTag(it)) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("1") |
| .assertIsDisplayed() |
| |
| rule.onNodeWithTag("2") |
| .assertDoesNotExist() |
| |
| rule.onNodeWithTag("3") |
| .assertDoesNotExist() |
| } |
| |
| @Test |
| fun adaptiveLazyGridAppliesHorizontalSpacings() { |
| val items = (1..3).map { it.toString() } |
| |
| val spacing = with(rule.density) { 10.toDp() } |
| val itemSize = with(rule.density) { 100.toDp() } |
| |
| rule.setContent { |
| LazyVerticalGrid( |
| cells = GridCells.Adaptive(itemSize), |
| modifier = Modifier.height(itemSize).width(itemSize * 3 + spacing * 2), |
| horizontalArrangement = Arrangement.spacedBy(spacing) |
| ) { |
| items(items) { |
| Spacer(Modifier.size(itemSize).testTag(it)) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("1") |
| .assertIsDisplayed() |
| .assertLeftPositionInRootIsEqualTo(0.dp) |
| .assertWidthIsAtLeast(itemSize) |
| |
| rule.onNodeWithTag("2") |
| .assertIsDisplayed() |
| .assertLeftPositionInRootIsEqualTo(itemSize + spacing) |
| .assertWidthIsAtLeast(itemSize) |
| |
| rule.onNodeWithTag("3") |
| .assertIsDisplayed() |
| .assertLeftPositionInRootIsEqualTo(itemSize * 2 + spacing * 2) |
| .assertWidthIsAtLeast(itemSize) |
| } |
| |
| @Test |
| fun adaptiveLazyGridAppliesHorizontalSpacingsWithContentPaddings() { |
| val items = (1..3).map { it.toString() } |
| |
| val spacing = with(rule.density) { 8.toDp() } |
| val itemSize = with(rule.density) { 40.toDp() } |
| |
| rule.setContent { |
| LazyVerticalGrid( |
| cells = GridCells.Adaptive(itemSize), |
| modifier = Modifier.height(itemSize).width(itemSize * 3 + spacing * 4), |
| horizontalArrangement = Arrangement.spacedBy(spacing), |
| contentPadding = PaddingValues(horizontal = spacing) |
| ) { |
| items(items) { |
| Spacer(Modifier.size(itemSize).testTag(it)) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("1") |
| .assertIsDisplayed() |
| .assertLeftPositionInRootIsEqualTo(spacing) |
| .assertWidthIsAtLeast(itemSize) |
| |
| rule.onNodeWithTag("2") |
| .assertIsDisplayed() |
| .assertLeftPositionInRootIsEqualTo(itemSize + spacing * 2) |
| .assertWidthIsAtLeast(itemSize) |
| |
| rule.onNodeWithTag("3") |
| .assertIsDisplayed() |
| .assertLeftPositionInRootIsEqualTo(itemSize * 2 + spacing * 3) |
| .assertWidthIsAtLeast(itemSize) |
| } |
| |
| @Test |
| fun adaptiveLazyGridAppliesVerticalSpacings() { |
| val items = (1..3).map { it.toString() } |
| |
| val spacing = with(rule.density) { 4.toDp() } |
| val itemSize = with(rule.density) { 32.toDp() } |
| |
| rule.setContent { |
| LazyVerticalGrid( |
| cells = GridCells.Adaptive(itemSize), |
| modifier = Modifier.height(itemSize * 3 + spacing * 2).width(itemSize), |
| verticalArrangement = Arrangement.spacedBy(spacing) |
| ) { |
| items(items) { |
| Spacer(Modifier.size(itemSize).testTag(it)) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("1") |
| .assertIsDisplayed() |
| .assertTopPositionInRootIsEqualTo(0.dp) |
| .assertHeightIsAtLeast(itemSize) |
| |
| rule.onNodeWithTag("2") |
| .assertIsDisplayed() |
| .assertTopPositionInRootIsEqualTo(itemSize + spacing) |
| .assertHeightIsAtLeast(itemSize) |
| |
| rule.onNodeWithTag("3") |
| .assertIsDisplayed() |
| .assertTopPositionInRootIsEqualTo(itemSize * 2 + spacing * 2) |
| .assertHeightIsAtLeast(itemSize) |
| } |
| |
| @Test |
| fun adaptiveLazyGridAppliesVerticalSpacingsWithContentPadding() { |
| val items = (1..3).map { it.toString() } |
| |
| val spacing = with(rule.density) { 16.toDp() } |
| val itemSize = with(rule.density) { 72.toDp() } |
| |
| rule.setContent { |
| LazyVerticalGrid( |
| cells = GridCells.Adaptive(itemSize), |
| modifier = Modifier.height(itemSize * 3 + spacing * 2).width(itemSize), |
| verticalArrangement = Arrangement.spacedBy(space = spacing), |
| contentPadding = PaddingValues(vertical = spacing) |
| ) { |
| items(items) { |
| Spacer(Modifier.size(itemSize).testTag(it)) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("1") |
| .assertIsDisplayed() |
| .assertTopPositionInRootIsEqualTo(spacing) |
| .assertHeightIsAtLeast(itemSize) |
| |
| rule.onNodeWithTag("2") |
| .assertIsDisplayed() |
| .assertTopPositionInRootIsEqualTo(spacing * 2 + itemSize) |
| .assertHeightIsAtLeast(itemSize) |
| |
| rule.onNodeWithTag("3") |
| .assertIsDisplayed() |
| .assertTopPositionInRootIsEqualTo(spacing * 3 + itemSize * 2) |
| .assertHeightIsAtLeast(itemSize) |
| } |
| |
| @Test |
| fun fixedLazyGridAppliesVerticalSpacings() { |
| val items = (1..4).map { it.toString() } |
| |
| val spacing = with(rule.density) { 24.toDp() } |
| val itemSize = with(rule.density) { 80.toDp() } |
| |
| rule.setContent { |
| LazyVerticalGrid( |
| cells = GridCells.Fixed(2), |
| modifier = Modifier.height(itemSize * 2 + spacing).width(itemSize), |
| verticalArrangement = Arrangement.spacedBy(space = spacing), |
| ) { |
| items(items) { |
| Spacer(Modifier.size(itemSize).testTag(it)) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("1") |
| .assertIsDisplayed() |
| .assertTopPositionInRootIsEqualTo(0.dp) |
| .assertHeightIsAtLeast(itemSize) |
| |
| rule.onNodeWithTag("2") |
| .assertIsDisplayed() |
| .assertTopPositionInRootIsEqualTo(0.dp) |
| .assertHeightIsAtLeast(itemSize) |
| |
| rule.onNodeWithTag("3") |
| .assertIsDisplayed() |
| .assertTopPositionInRootIsEqualTo(spacing + itemSize) |
| .assertHeightIsAtLeast(itemSize) |
| |
| rule.onNodeWithTag("4") |
| .assertIsDisplayed() |
| .assertTopPositionInRootIsEqualTo(spacing + itemSize) |
| .assertHeightIsAtLeast(itemSize) |
| } |
| |
| @Test |
| fun fixedLazyGridAppliesHorizontalSpacings() { |
| val items = (1..4).map { it.toString() } |
| |
| val spacing = with(rule.density) { 15.toDp() } |
| val itemSize = with(rule.density) { 30.toDp() } |
| |
| rule.setContent { |
| LazyVerticalGrid( |
| cells = GridCells.Fixed(2), |
| modifier = Modifier.height(itemSize * 2).width(itemSize * 2 + spacing), |
| horizontalArrangement = Arrangement.spacedBy(space = spacing), |
| ) { |
| items(items) { |
| Spacer(Modifier.size(itemSize).testTag(it)) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("1") |
| .assertIsDisplayed() |
| .assertLeftPositionInRootIsEqualTo(0.dp) |
| .assertWidthIsAtLeast(itemSize) |
| |
| rule.onNodeWithTag("2") |
| .assertIsDisplayed() |
| .assertLeftPositionInRootIsEqualTo(spacing + itemSize) |
| .assertWidthIsAtLeast(itemSize) |
| |
| rule.onNodeWithTag("3") |
| .assertIsDisplayed() |
| .assertLeftPositionInRootIsEqualTo(0.dp) |
| .assertWidthIsAtLeast(itemSize) |
| |
| rule.onNodeWithTag("4") |
| .assertIsDisplayed() |
| .assertLeftPositionInRootIsEqualTo(spacing + itemSize) |
| .assertWidthIsAtLeast(itemSize) |
| } |
| |
| @Test |
| fun fixedLazyGridAppliesVerticalSpacingsWithContentPadding() { |
| val items = (1..4).map { it.toString() } |
| |
| val spacing = with(rule.density) { 30.toDp() } |
| val itemSize = with(rule.density) { 77.toDp() } |
| |
| rule.setContent { |
| LazyVerticalGrid( |
| cells = GridCells.Fixed(2), |
| modifier = Modifier.height(itemSize * 2 + spacing).width(itemSize), |
| verticalArrangement = Arrangement.spacedBy(space = spacing), |
| contentPadding = PaddingValues(vertical = spacing) |
| ) { |
| items(items) { |
| Spacer(Modifier.size(itemSize).testTag(it)) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("1") |
| .assertIsDisplayed() |
| .assertTopPositionInRootIsEqualTo(spacing) |
| .assertHeightIsAtLeast(itemSize) |
| |
| rule.onNodeWithTag("2") |
| .assertIsDisplayed() |
| .assertTopPositionInRootIsEqualTo(spacing) |
| .assertHeightIsAtLeast(itemSize) |
| |
| rule.onNodeWithTag("3") |
| .assertIsDisplayed() |
| .assertTopPositionInRootIsEqualTo(spacing * 2 + itemSize) |
| .assertHeightIsAtLeast(itemSize) |
| |
| rule.onNodeWithTag("4") |
| .assertIsDisplayed() |
| .assertTopPositionInRootIsEqualTo(spacing * 2 + itemSize) |
| .assertHeightIsAtLeast(itemSize) |
| } |
| |
| @Test |
| fun fixedLazyGridAppliesHorizontalSpacingsWithContentPadding() { |
| val items = (1..4).map { it.toString() } |
| |
| val spacing = with(rule.density) { 22.toDp() } |
| val itemSize = with(rule.density) { 44.toDp() } |
| |
| rule.setContent { |
| LazyVerticalGrid( |
| cells = GridCells.Fixed(2), |
| modifier = Modifier.height(itemSize * 2).width(itemSize * 2 + spacing * 3), |
| horizontalArrangement = Arrangement.spacedBy(space = spacing), |
| contentPadding = PaddingValues(horizontal = spacing) |
| ) { |
| items(items) { |
| Spacer(Modifier.size(itemSize).testTag(it)) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("1") |
| .assertIsDisplayed() |
| .assertLeftPositionInRootIsEqualTo(spacing) |
| .assertWidthIsAtLeast(itemSize) |
| |
| rule.onNodeWithTag("2") |
| .assertIsDisplayed() |
| .assertLeftPositionInRootIsEqualTo(spacing * 2 + itemSize) |
| .assertWidthIsAtLeast(itemSize) |
| |
| rule.onNodeWithTag("3") |
| .assertIsDisplayed() |
| .assertLeftPositionInRootIsEqualTo(spacing) |
| .assertWidthIsAtLeast(itemSize) |
| |
| rule.onNodeWithTag("4") |
| .assertIsDisplayed() |
| .assertLeftPositionInRootIsEqualTo(spacing * 2 + itemSize) |
| .assertWidthIsAtLeast(itemSize) |
| } |
| |
| @Test |
| fun usedWithArray() { |
| val items = arrayOf("1", "2", "3", "4") |
| |
| val itemSize = with(rule.density) { 15.toDp() } |
| |
| rule.setContent { |
| LazyVerticalGrid( |
| GridCells.Fixed(2), |
| Modifier.requiredWidth(itemSize * 2) |
| ) { |
| items(items) { |
| Spacer(Modifier.requiredHeight(itemSize).testTag(it)) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("1") |
| .assertTopPositionInRootIsEqualTo(0.dp) |
| .assertLeftPositionInRootIsEqualTo(0.dp) |
| |
| rule.onNodeWithTag("2") |
| .assertTopPositionInRootIsEqualTo(0.dp) |
| .assertLeftPositionInRootIsEqualTo(itemSize) |
| |
| rule.onNodeWithTag("3") |
| .assertTopPositionInRootIsEqualTo(itemSize) |
| .assertLeftPositionInRootIsEqualTo(0.dp) |
| |
| rule.onNodeWithTag("4") |
| .assertTopPositionInRootIsEqualTo(itemSize) |
| .assertLeftPositionInRootIsEqualTo(itemSize) |
| } |
| |
| @Test |
| fun usedWithArrayIndexed() { |
| val items = arrayOf("1", "2", "3", "4") |
| |
| val itemSize = with(rule.density) { 15.toDp() } |
| |
| rule.setContent { |
| LazyVerticalGrid( |
| GridCells.Fixed(2), |
| Modifier.requiredWidth(itemSize * 2) |
| ) { |
| itemsIndexed(items) { index, item -> |
| Spacer(Modifier.requiredHeight(itemSize).testTag("$index*$item")) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("0*1") |
| .assertTopPositionInRootIsEqualTo(0.dp) |
| .assertLeftPositionInRootIsEqualTo(0.dp) |
| |
| rule.onNodeWithTag("1*2") |
| .assertTopPositionInRootIsEqualTo(0.dp) |
| .assertLeftPositionInRootIsEqualTo(itemSize) |
| |
| rule.onNodeWithTag("2*3") |
| .assertTopPositionInRootIsEqualTo(itemSize) |
| .assertLeftPositionInRootIsEqualTo(0.dp) |
| |
| rule.onNodeWithTag("3*4") |
| .assertTopPositionInRootIsEqualTo(itemSize) |
| .assertLeftPositionInRootIsEqualTo(itemSize) |
| } |
| |
| @Test |
| fun changeItemsCountAndScrollImmediately() { |
| lateinit var state: LazyGridState |
| var count by mutableStateOf(100) |
| val composedIndexes = mutableListOf<Int>() |
| rule.setContent { |
| state = rememberLazyGridState() |
| LazyVerticalGrid( |
| GridCells.Fixed(1), |
| Modifier.fillMaxWidth().height(10.dp), |
| state |
| ) { |
| items(count) { index -> |
| composedIndexes.add(index) |
| Box(Modifier.size(20.dp)) |
| } |
| } |
| } |
| |
| rule.runOnIdle { |
| composedIndexes.clear() |
| count = 10 |
| runBlocking(AutoTestFrameClock()) { |
| // we try to scroll to the index after 10, but we expect that the component will |
| // already be aware there is a new count and not compose items with index > 10 |
| state.scrollToItem(50) |
| } |
| composedIndexes.forEach { |
| Truth.assertThat(it).isLessThan(count) |
| } |
| Truth.assertThat(state.firstVisibleItemIndex).isEqualTo(9) |
| } |
| } |
| |
| @Test |
| fun maxIntElements() { |
| val itemSize = with(rule.density) { 15.toDp() } |
| |
| rule.setContent { |
| LazyVerticalGrid( |
| cells = GridCells.Fixed(2), |
| modifier = Modifier.requiredSize(itemSize * 2).testTag(LazyGridTag), |
| state = LazyGridState(firstVisibleItemIndex = Int.MAX_VALUE - 3) |
| ) { |
| items(Int.MAX_VALUE) { |
| Box(Modifier.size(itemSize).testTag("$it")) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("${Int.MAX_VALUE - 3}") |
| .assertTopPositionInRootIsEqualTo(0.dp) |
| .assertLeftPositionInRootIsEqualTo(0.dp) |
| |
| rule.onNodeWithTag("${Int.MAX_VALUE - 2}") |
| .assertTopPositionInRootIsEqualTo(0.dp) |
| .assertLeftPositionInRootIsEqualTo(itemSize) |
| |
| rule.onNodeWithTag("${Int.MAX_VALUE - 1}") |
| .assertTopPositionInRootIsEqualTo(itemSize) |
| .assertLeftPositionInRootIsEqualTo(0.dp) |
| |
| rule.onNodeWithTag("${Int.MAX_VALUE}").assertDoesNotExist() |
| rule.onNodeWithTag("0").assertDoesNotExist() |
| } |
| |
| @Test |
| fun spans() { |
| val columns = 4 |
| val columnWidth = with(rule.density) { 5.toDp() } |
| val itemHeight = with(rule.density) { 10.toDp() } |
| rule.setContent { |
| LazyVerticalGrid( |
| cells = GridCells.Fixed(columns), |
| modifier = Modifier.requiredSize(columnWidth * columns, itemHeight * 3) |
| ) { |
| items( |
| count = 6, |
| span = { index -> |
| when (index) { |
| 0 -> { |
| Truth.assertThat(itemRow).isEqualTo(0) |
| Truth.assertThat(itemColumn).isEqualTo(0) |
| Truth.assertThat(maxCurrentLineSpan).isEqualTo(4) |
| GridItemSpan(3) |
| } |
| 1 -> { |
| Truth.assertThat(itemRow).isEqualTo(0) |
| Truth.assertThat(itemColumn).isEqualTo(3) |
| Truth.assertThat(maxCurrentLineSpan).isEqualTo(1) |
| GridItemSpan(1) |
| } |
| 2 -> { |
| Truth.assertThat(itemRow).isEqualTo(1) |
| Truth.assertThat(itemColumn).isEqualTo(0) |
| Truth.assertThat(maxCurrentLineSpan).isEqualTo(4) |
| GridItemSpan(1) |
| } |
| 3 -> { |
| Truth.assertThat(itemRow).isEqualTo(1) |
| Truth.assertThat(itemColumn).isEqualTo(1) |
| Truth.assertThat(maxCurrentLineSpan).isEqualTo(3) |
| GridItemSpan(3) |
| } |
| 4 -> { |
| Truth.assertThat(itemRow).isEqualTo(2) |
| Truth.assertThat(itemColumn).isEqualTo(0) |
| Truth.assertThat(maxCurrentLineSpan).isEqualTo(4) |
| GridItemSpan(1) |
| } |
| 5 -> { |
| Truth.assertThat(itemRow).isEqualTo(2) |
| Truth.assertThat(itemColumn).isEqualTo(1) |
| Truth.assertThat(maxCurrentLineSpan).isEqualTo(3) |
| GridItemSpan(1) |
| } |
| else -> error("Out of index span queried") |
| } |
| }, |
| ) { |
| Box(Modifier.fillMaxWidth().height(itemHeight).testTag("$it")) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("0") |
| .assertTopPositionInRootIsEqualTo(0.dp) |
| .assertLeftPositionInRootIsEqualTo(0.dp) |
| rule.onNodeWithTag("1") |
| .assertTopPositionInRootIsEqualTo(0.dp) |
| .assertLeftPositionInRootIsEqualTo(columnWidth * 3) |
| rule.onNodeWithTag("2") |
| .assertTopPositionInRootIsEqualTo(itemHeight) |
| .assertLeftPositionInRootIsEqualTo(0.dp) |
| rule.onNodeWithTag("3") |
| .assertTopPositionInRootIsEqualTo(itemHeight) |
| .assertLeftPositionInRootIsEqualTo(columnWidth) |
| rule.onNodeWithTag("4") |
| .assertTopPositionInRootIsEqualTo(itemHeight * 2) |
| .assertLeftPositionInRootIsEqualTo(0.dp) |
| rule.onNodeWithTag("5") |
| .assertTopPositionInRootIsEqualTo(itemHeight * 2) |
| .assertLeftPositionInRootIsEqualTo(columnWidth) |
| } |
| |
| @Test |
| fun spansWithHorizontalSpacing() { |
| val columns = 4 |
| val columnWidth = with(rule.density) { 5.toDp() } |
| val itemHeight = with(rule.density) { 10.toDp() } |
| val spacing = with(rule.density) { 4.toDp() } |
| rule.setContent { |
| LazyVerticalGrid( |
| cells = GridCells.Fixed(columns), |
| modifier = Modifier.requiredSize( |
| columnWidth * columns + spacing * (columns - 1), |
| itemHeight |
| ), |
| horizontalArrangement = Arrangement.spacedBy(spacing) |
| ) { |
| items( |
| count = 2, |
| span = { index -> |
| when (index) { |
| 0 -> GridItemSpan(1) |
| 1 -> GridItemSpan(3) |
| else -> error("Out of index span queried") |
| } |
| } |
| ) { |
| Box(Modifier.fillMaxWidth().height(itemHeight).testTag("$it")) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("0") |
| .assertTopPositionInRootIsEqualTo(0.dp) |
| .assertLeftPositionInRootIsEqualTo(0.dp) |
| .assertWidthIsEqualTo(columnWidth) |
| rule.onNodeWithTag("1") |
| .assertTopPositionInRootIsEqualTo(0.dp) |
| .assertLeftPositionInRootIsEqualTo(columnWidth + spacing) |
| .assertWidthIsEqualTo(columnWidth * 3 + spacing * 2) |
| } |
| |
| @Test |
| fun spansMultipleBlocks() { |
| val columns = 4 |
| val columnWidth = with(rule.density) { 5.toDp() } |
| val itemHeight = with(rule.density) { 10.toDp() } |
| rule.setContent { |
| LazyVerticalGrid( |
| cells = GridCells.Fixed(columns), |
| modifier = Modifier.requiredSize(columnWidth * columns, itemHeight) |
| ) { |
| items( |
| count = 1, |
| span = { index -> |
| when (index) { |
| 0 -> GridItemSpan(1) |
| else -> error("Out of index span queried") |
| } |
| } |
| ) { |
| Box(Modifier.fillMaxWidth().height(itemHeight).testTag("0")) |
| } |
| item(span = { |
| if (maxCurrentLineSpan != 3) error("Wrong maxSpan") |
| GridItemSpan(2) |
| }) { |
| Box(Modifier.fillMaxWidth().height(itemHeight).testTag("1")) |
| } |
| items( |
| count = 1, |
| span = { index -> |
| if (maxCurrentLineSpan != 1 || index != 0) { |
| error("Wrong span calculation parameters") |
| } |
| GridItemSpan(1) |
| } |
| ) { |
| if (it != 0) error("Wrong index") |
| Box(Modifier.fillMaxWidth().height(itemHeight).testTag("2")) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("0") |
| .assertTopPositionInRootIsEqualTo(0.dp) |
| .assertLeftPositionInRootIsEqualTo(0.dp) |
| .assertWidthIsEqualTo(columnWidth) |
| rule.onNodeWithTag("1") |
| .assertTopPositionInRootIsEqualTo(0.dp) |
| .assertLeftPositionInRootIsEqualTo(columnWidth) |
| .assertWidthIsEqualTo(columnWidth * 2) |
| rule.onNodeWithTag("2") |
| .assertTopPositionInRootIsEqualTo(0.dp) |
| .assertLeftPositionInRootIsEqualTo(columnWidth * 3) |
| .assertWidthIsEqualTo(columnWidth) |
| } |
| |
| @Test |
| fun pointerInputScrollingIsAllowedWhenUserScrollingIsEnabled() { |
| val itemSize = with(rule.density) { 30.toDp() } |
| rule.setContentWithTestViewConfiguration { |
| LazyVerticalGrid( |
| GridCells.Fixed(1), |
| Modifier.size(itemSize * 3).testTag(LazyGridTag), |
| userScrollEnabled = true, |
| ) { |
| items(5) { |
| Spacer(Modifier.size(itemSize).testTag("$it")) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag(LazyGridTag).scrollBy(y = itemSize, density = rule.density) |
| |
| rule.onNodeWithTag("1") |
| .assertTopPositionInRootIsEqualTo(0.dp) |
| } |
| |
| @Test |
| fun pointerInputScrollingIsDisallowedWhenUserScrollingIsDisabled() { |
| val itemSize = with(rule.density) { 30.toDp() } |
| rule.setContentWithTestViewConfiguration { |
| LazyVerticalGrid( |
| GridCells.Fixed(1), |
| Modifier.size(itemSize * 3).testTag(LazyGridTag), |
| userScrollEnabled = false, |
| ) { |
| items(5) { |
| Spacer(Modifier.size(itemSize).testTag("$it")) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag(LazyGridTag).scrollBy(y = itemSize, density = rule.density) |
| |
| rule.onNodeWithTag("1") |
| .assertTopPositionInRootIsEqualTo(itemSize) |
| } |
| |
| @Test |
| fun programmaticScrollingIsAllowedWhenUserScrollingIsDisabled() { |
| val itemSizePx = 30f |
| val itemSize = with(rule.density) { itemSizePx.toDp() } |
| lateinit var state: LazyGridState |
| rule.setContentWithTestViewConfiguration { |
| LazyVerticalGrid( |
| GridCells.Fixed(1), |
| Modifier.size(itemSize * 3), |
| state = rememberLazyGridState().also { state = it }, |
| userScrollEnabled = false, |
| ) { |
| items(5) { |
| Spacer(Modifier.size(itemSize).testTag("$it")) |
| } |
| } |
| } |
| |
| rule.runOnIdle { |
| runBlocking { |
| state.scrollBy(itemSizePx) |
| } |
| } |
| |
| rule.onNodeWithTag("1") |
| .assertTopPositionInRootIsEqualTo(0.dp) |
| } |
| |
| @Test |
| fun semanticScrollingIsDisallowedWhenUserScrollingIsDisabled() { |
| val itemSize = with(rule.density) { 30.toDp() } |
| rule.setContentWithTestViewConfiguration { |
| LazyVerticalGrid( |
| GridCells.Fixed(1), |
| Modifier.size(itemSize * 3).testTag(LazyGridTag), |
| userScrollEnabled = false, |
| ) { |
| items(5) { |
| Spacer(Modifier.size(itemSize).testTag("$it")) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag(LazyGridTag) |
| .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.ScrollBy)) |
| .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.ScrollToIndex)) |
| // but we still have a read only scroll range property |
| .assert(keyIsDefined(SemanticsProperties.VerticalScrollAxisRange)) |
| } |
| |
| @Test |
| fun rtl() { |
| val gridWidth = 30 |
| val gridWidthDp = with(rule.density) { gridWidth.toDp() } |
| rule.setContent { |
| CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { |
| LazyVerticalGrid(GridCells.Fixed(3), Modifier.width(gridWidthDp)) { |
| items(3) { |
| Box(Modifier.height(1.dp).testTag("$it")) |
| } |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("0").assertLeftPositionInRootIsEqualTo(gridWidthDp * 2 / 3) |
| rule.onNodeWithTag("1").assertLeftPositionInRootIsEqualTo(gridWidthDp / 3) |
| rule.onNodeWithTag("2").assertLeftPositionInRootIsEqualTo(0.dp) |
| } |
| |
| @Test |
| fun withMissingItems() { |
| val itemHeight = with(rule.density) { 30.toDp() } |
| lateinit var state: LazyGridState |
| rule.setContent { |
| state = rememberLazyGridState() |
| LazyVerticalGrid( |
| cells = GridCells.Fixed(2), |
| modifier = Modifier.height(itemHeight + 1.dp), |
| state = state |
| ) { |
| items((0..8).map { it.toString() }) { |
| if (it != "3") { |
| Box(Modifier.size(itemHeight).testTag(it)) |
| } |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("0").assertIsDisplayed() |
| rule.onNodeWithTag("1").assertIsDisplayed() |
| rule.onNodeWithTag("2").assertIsDisplayed() |
| |
| rule.runOnIdle { |
| runBlocking { |
| state.scrollToItem(3) |
| } |
| } |
| |
| rule.onNodeWithTag("0").assertIsNotDisplayed() |
| rule.onNodeWithTag("1").assertIsNotDisplayed() |
| rule.onNodeWithTag("2").assertIsDisplayed() |
| rule.onNodeWithTag("4").assertIsDisplayed() |
| rule.onNodeWithTag("5").assertIsDisplayed() |
| rule.onNodeWithTag("6").assertDoesNotExist() |
| rule.onNodeWithTag("7").assertDoesNotExist() |
| } |
| |
| @Test |
| fun recomposingWithNewComposedModifierObjectIsNotCausingRemeasure() { |
| var remeasureCount = 0 |
| val layoutModifier = Modifier.layout { measurable, constraints -> |
| remeasureCount++ |
| val placeable = measurable.measure(constraints) |
| layout(placeable.width, placeable.height) { |
| placeable.place(0, 0) |
| } |
| } |
| val counter = mutableStateOf(0) |
| |
| rule.setContentWithTestViewConfiguration { |
| counter.value // just to trigger recomposition |
| LazyVerticalGrid( |
| GridCells.Fixed(1), |
| // this will return a new object everytime causing LazyVerticalGrid recomposition |
| // without causing remeasure |
| Modifier.composed { layoutModifier } |
| ) { |
| items(1) { |
| Spacer(Modifier.size(10.dp)) |
| } |
| } |
| } |
| |
| rule.runOnIdle { |
| Truth.assertThat(remeasureCount).isEqualTo(1) |
| counter.value++ |
| } |
| |
| rule.runOnIdle { |
| Truth.assertThat(remeasureCount).isEqualTo(1) |
| } |
| } |
| |
| @Test |
| fun scrollingALotDoesntCauseLazyLayoutRecomposition() { |
| var recomposeCount = 0 |
| lateinit var state: LazyGridState |
| |
| rule.setContentWithTestViewConfiguration { |
| state = rememberLazyGridState() |
| LazyVerticalGrid( |
| GridCells.Fixed(1), |
| Modifier.composed { |
| recomposeCount++ |
| Modifier |
| }.size(100.dp), |
| state |
| ) { |
| items(1000) { |
| Spacer(Modifier.size(100.dp)) |
| } |
| } |
| } |
| |
| rule.runOnIdle { |
| Truth.assertThat(recomposeCount).isEqualTo(1) |
| |
| runBlocking { |
| state.scrollToItem(100) |
| } |
| } |
| |
| rule.runOnIdle { |
| Truth.assertThat(recomposeCount).isEqualTo(1) |
| } |
| } |
| |
| // TODO: add tests for the cache logic |
| } |