1 /* <lambda>null2 * Copyright 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * https://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.accompanist.navigation.material 18 19 import androidx.compose.foundation.layout.Box 20 import androidx.compose.foundation.layout.ColumnScope 21 import androidx.compose.foundation.layout.fillMaxSize 22 import androidx.compose.foundation.layout.height 23 import androidx.compose.material.ExperimentalMaterialApi 24 import androidx.compose.material.ModalBottomSheetLayout 25 import androidx.compose.material.ModalBottomSheetState 26 import androidx.compose.material.ModalBottomSheetValue 27 import androidx.compose.material.Text 28 import androidx.compose.runtime.Composable 29 import androidx.compose.runtime.LaunchedEffect 30 import androidx.compose.runtime.State 31 import androidx.compose.runtime.mutableStateOf 32 import androidx.compose.runtime.saveable.rememberSaveableStateHolder 33 import androidx.compose.ui.Modifier 34 import androidx.compose.ui.platform.testTag 35 import androidx.compose.ui.test.junit4.ComposeContentTestRule 36 import androidx.compose.ui.test.junit4.createComposeRule 37 import androidx.compose.ui.test.onNodeWithTag 38 import androidx.compose.ui.test.performClick 39 import androidx.compose.ui.unit.dp 40 import androidx.navigation.NavBackStackEntry 41 import androidx.navigation.testing.TestNavigatorState 42 import androidx.test.ext.junit.runners.AndroidJUnit4 43 import androidx.test.filters.LargeTest 44 import com.google.common.truth.Truth.assertThat 45 import com.google.common.truth.Truth.assertWithMessage 46 import kotlinx.coroutines.ExperimentalCoroutinesApi 47 import kotlinx.coroutines.test.runTest 48 import org.junit.Rule 49 import org.junit.Test 50 import org.junit.runner.RunWith 51 52 @LargeTest 53 @RunWith(AndroidJUnit4::class) 54 @OptIn( 55 ExperimentalCoroutinesApi::class, 56 ExperimentalMaterialApi::class, 57 ExperimentalMaterialNavigationApi::class 58 ) 59 internal class SheetContentHostTest { 60 private val bodyContentTag = "testBodyContent" 61 62 @get:Rule 63 val composeTestRule = createComposeRule() 64 65 @Test 66 fun testOnSheetDismissedCalled_ManualDismiss() = runTest { 67 val sheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden, composeTestRule.density) 68 val backStackEntry = createBackStackEntry(sheetState) 69 70 val dismissedBackStackEntries = mutableListOf<NavBackStackEntry>() 71 72 composeTestRule.setBottomSheetContent( 73 mutableStateOf(backStackEntry), 74 sheetState, 75 onSheetShown = { }, 76 onSheetDismissed = { entry -> dismissedBackStackEntries.add(entry) } 77 ) 78 79 assertThat(sheetState.currentValue == ModalBottomSheetValue.Expanded).isTrue() 80 composeTestRule.onNodeWithTag(bodyContentTag).performClick() 81 composeTestRule.runOnIdle { 82 assertWithMessage("Sheet is visible") 83 .that(sheetState.isVisible).isFalse() 84 assertWithMessage("Back stack entry should be in the dismissed entries list") 85 .that(dismissedBackStackEntries) 86 .containsExactly(backStackEntry) 87 } 88 } 89 90 @Test 91 fun testOnSheetDismissedCalled_initiallyExpanded() = runTest { 92 val sheetState = ModalBottomSheetState(ModalBottomSheetValue.Expanded, composeTestRule.density) 93 val backStackEntry = createBackStackEntry(sheetState) 94 95 val dismissedBackStackEntries = mutableListOf<NavBackStackEntry>() 96 97 composeTestRule.setBottomSheetContent( 98 mutableStateOf(backStackEntry), 99 sheetState, 100 onSheetShown = { }, 101 onSheetDismissed = { entry -> dismissedBackStackEntries.add(entry) } 102 ) 103 104 assertThat(sheetState.currentValue == ModalBottomSheetValue.Expanded).isTrue() 105 composeTestRule.onNodeWithTag(bodyContentTag).performClick() 106 composeTestRule.runOnIdle { 107 assertWithMessage("Sheet is not visible") 108 .that(sheetState.isVisible).isFalse() 109 assertWithMessage("Back stack entry should be in the dismissed entries list") 110 .that(dismissedBackStackEntries) 111 .containsExactly(backStackEntry) 112 } 113 } 114 115 @Test 116 fun testOnSheetShownCalled_onBackStackEntryEnter_shortSheet() = runTest { 117 val sheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden, composeTestRule.density) 118 val backStackEntryState = mutableStateOf<NavBackStackEntry?>(null) 119 val shownBackStackEntries = mutableListOf<NavBackStackEntry>() 120 121 composeTestRule.setBottomSheetContent( 122 backStackEntry = backStackEntryState, 123 sheetState = sheetState, 124 onSheetShown = { entry -> shownBackStackEntries.add(entry) }, 125 onSheetDismissed = { } 126 ) 127 128 val backStackEntry = createBackStackEntry(sheetState) { 129 Box(Modifier.height(50.dp)) 130 } 131 backStackEntryState.value = backStackEntry 132 133 composeTestRule.runOnIdle { 134 assertWithMessage("Sheet is visible") 135 .that(sheetState.isVisible).isTrue() 136 assertWithMessage("Back stack entry should be in the shown entries list") 137 .that(shownBackStackEntries) 138 .containsExactly(backStackEntry) 139 } 140 } 141 142 @Test 143 fun testOnSheetShownCalled_onBackStackEntryEnter_tallSheet() = runTest { 144 val sheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden, composeTestRule.density) 145 val backStackEntryState = mutableStateOf<NavBackStackEntry?>(null) 146 val shownBackStackEntries = mutableListOf<NavBackStackEntry>() 147 148 composeTestRule.setBottomSheetContent( 149 backStackEntry = backStackEntryState, 150 sheetState = sheetState, 151 onSheetShown = { entry -> shownBackStackEntries.add(entry) }, 152 onSheetDismissed = { } 153 ) 154 155 val backStackEntry = createBackStackEntry(sheetState) { 156 Box(Modifier.fillMaxSize()) 157 } 158 backStackEntryState.value = backStackEntry 159 160 composeTestRule.runOnIdle { 161 assertWithMessage("Sheet is visible") 162 .that(sheetState.isVisible).isTrue() 163 assertWithMessage("Back stack entry should be in the shown entries list") 164 .that(shownBackStackEntries) 165 .containsExactly(backStackEntry) 166 } 167 } 168 169 private fun ComposeContentTestRule.setBottomSheetContent( 170 backStackEntry: State<NavBackStackEntry?>, 171 sheetState: ModalBottomSheetState, 172 onSheetShown: (NavBackStackEntry) -> Unit, 173 onSheetDismissed: (NavBackStackEntry) -> Unit 174 ) { 175 setContent { 176 val saveableStateHolder = rememberSaveableStateHolder() 177 LaunchedEffect(backStackEntry.value) { 178 if (backStackEntry.value == null) sheetState.hide() else sheetState.show() 179 } 180 ModalBottomSheetLayout( 181 sheetContent = { 182 SheetContentHost( 183 backStackEntry = backStackEntry.value, 184 sheetState = sheetState, 185 saveableStateHolder = saveableStateHolder, 186 onSheetShown = onSheetShown, 187 onSheetDismissed = onSheetDismissed 188 ) 189 }, 190 sheetState = sheetState, 191 content = { 192 Box( 193 Modifier 194 .fillMaxSize() 195 .testTag(bodyContentTag) 196 ) 197 } 198 ) 199 } 200 } 201 202 private fun createBackStackEntry( 203 sheetState: ModalBottomSheetState, 204 sheetContent: @Composable ColumnScope.(NavBackStackEntry) -> Unit = { Text("Fake Sheet Content") } 205 ): NavBackStackEntry { 206 val navigatorState = TestNavigatorState() 207 val navigator = BottomSheetNavigator(sheetState) 208 navigator.onAttach(navigatorState) 209 210 val destination = BottomSheetNavigator.Destination(navigator, sheetContent) 211 val backStackEntry = navigatorState.createBackStackEntry(destination, null) 212 navigator.navigate(listOf(backStackEntry), null, null) 213 return backStackEntry 214 } 215 } 216