• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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