• 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 android.os.Bundle
20 import androidx.activity.OnBackPressedDispatcher
21 import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
22 import androidx.compose.animation.core.tween
23 import androidx.compose.foundation.background
24 import androidx.compose.foundation.layout.Box
25 import androidx.compose.foundation.layout.Column
26 import androidx.compose.foundation.layout.fillMaxSize
27 import androidx.compose.foundation.layout.fillMaxWidth
28 import androidx.compose.foundation.layout.height
29 import androidx.compose.foundation.layout.size
30 import androidx.compose.material.Button
31 import androidx.compose.material.ExperimentalMaterialApi
32 import androidx.compose.material.ModalBottomSheetState
33 import androidx.compose.material.ModalBottomSheetValue
34 import androidx.compose.material.Text
35 import androidx.compose.runtime.DisposableEffect
36 import androidx.compose.runtime.getValue
37 import androidx.compose.runtime.mutableStateOf
38 import androidx.compose.runtime.remember
39 import androidx.compose.runtime.setValue
40 import androidx.compose.ui.Modifier
41 import androidx.compose.ui.graphics.Color
42 import androidx.compose.ui.platform.testTag
43 import androidx.compose.ui.test.click
44 import androidx.compose.ui.test.getUnclippedBoundsInRoot
45 import androidx.compose.ui.test.junit4.createComposeRule
46 import androidx.compose.ui.test.onNodeWithTag
47 import androidx.compose.ui.test.onNodeWithText
48 import androidx.compose.ui.test.onRoot
49 import androidx.compose.ui.test.performClick
50 import androidx.compose.ui.test.performTouchInput
51 import androidx.compose.ui.unit.Dp
52 import androidx.compose.ui.unit.dp
53 import androidx.compose.ui.unit.height
54 import androidx.lifecycle.Lifecycle
55 import androidx.navigation.NavBackStackEntry
56 import androidx.navigation.NavHostController
57 import androidx.navigation.compose.NavHost
58 import androidx.navigation.compose.composable
59 import androidx.navigation.compose.rememberNavController
60 import androidx.navigation.testing.TestNavigatorState
61 import androidx.test.ext.junit.runners.AndroidJUnit4
62 import androidx.test.filters.LargeTest
63 import com.google.common.truth.Truth.assertThat
64 import com.google.common.truth.Truth.assertWithMessage
65 import kotlinx.coroutines.runBlocking
66 import org.junit.Rule
67 import org.junit.Test
68 import org.junit.runner.RunWith
69 import kotlin.math.roundToLong
70 
71 @LargeTest
72 @RunWith(AndroidJUnit4::class)
73 @OptIn(ExperimentalMaterialApi::class, ExperimentalMaterialNavigationApi::class)
74 internal class BottomSheetNavigatorTest {
75 
76     @get:Rule
77     val composeTestRule = createComposeRule()
78 
79     @Test
80     fun testNavigateAddsDestinationToBackStack(): Unit = runBlocking {
81         val sheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden, composeTestRule.density)
82         val navigatorState = TestNavigatorState()
83         val navigator = BottomSheetNavigator(sheetState)
84 
85         navigator.onAttach(navigatorState)
86         val entry = navigatorState.createBackStackEntry(navigator.createFakeDestination(), null)
87         navigator.navigate(listOf(entry), null, null)
88 
89         assertWithMessage("The back stack entry has been added to the back stack")
90             .that(navigatorState.backStack.value)
91             .containsExactly(entry)
92     }
93 
94     @Test
95     fun testNavigateAddsDestinationToBackStackAndKeepsPrevious(): Unit = runBlocking {
96         val sheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden, composeTestRule.density)
97         val navigator = BottomSheetNavigator(sheetState)
98         val navigatorState = TestNavigatorState()
99 
100         navigator.onAttach(navigatorState)
101         val firstEntry = navigatorState.createBackStackEntry(navigator.createFakeDestination(), null)
102         val secondEntry = navigatorState.createBackStackEntry(navigator.createFakeDestination(), null)
103 
104         navigator.navigate(listOf(firstEntry), null, null)
105         assertWithMessage("The first entry has been added to the back stack")
106             .that(navigatorState.backStack.value)
107             .containsExactly(firstEntry)
108 
109         navigator.navigate(listOf(secondEntry), null, null)
110         assertWithMessage(
111             "The second entry has been added to the back stack and it still " +
112                 "contains the first entry"
113         )
114             .that(navigatorState.backStack.value)
115             .containsExactly(firstEntry, secondEntry)
116             .inOrder()
117     }
118 
119     @Test
120     fun testNavigateComposesDestinationAndDisposesPreviousDestination(): Unit = runBlocking {
121         val sheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden, composeTestRule.density)
122         val navigator = BottomSheetNavigator(sheetState)
123         val navigatorState = TestNavigatorState()
124         navigator.onAttach(navigatorState)
125 
126         composeTestRule.setContent {
127             Column { navigator.sheetContent(this) }
128         }
129 
130         var firstDestinationCompositions = 0
131         val firstDestinationContentTag = "firstSheetContentTest"
132         val firstDestination = BottomSheetNavigator.Destination(navigator) {
133             DisposableEffect(Unit) {
134                 firstDestinationCompositions++
135                 onDispose { firstDestinationCompositions = 0 }
136             }
137             Text("Fake Sheet Content", Modifier.testTag(firstDestinationContentTag))
138         }
139         val firstEntry = navigatorState.createBackStackEntry(firstDestination, null)
140 
141         var secondDestinationCompositions = 0
142         val secondDestinationContentTag = "secondSheetContentTest"
143         val secondDestination = BottomSheetNavigator.Destination(navigator) {
144             DisposableEffect(Unit) {
145                 secondDestinationCompositions++
146                 onDispose { secondDestinationCompositions = 0 }
147             }
148             Box(
149                 Modifier
150                     .size(64.dp)
151                     .testTag(secondDestinationContentTag)
152             )
153         }
154         val secondEntry = navigatorState.createBackStackEntry(secondDestination, null)
155 
156         navigator.navigate(listOf(firstEntry), null, null)
157         composeTestRule.awaitIdle()
158 
159         composeTestRule.onNodeWithTag(firstDestinationContentTag).assertExists()
160         composeTestRule.onNodeWithTag(secondDestinationContentTag).assertDoesNotExist()
161         assertWithMessage("First destination should have been composed exactly once")
162             .that(firstDestinationCompositions).isEqualTo(1)
163         assertWithMessage("Second destination should not have been composed yet")
164             .that(secondDestinationCompositions).isEqualTo(0)
165 
166         navigator.navigate(listOf(secondEntry), null, null)
167         composeTestRule.awaitIdle()
168 
169         composeTestRule.onNodeWithTag(firstDestinationContentTag).assertDoesNotExist()
170         composeTestRule.onNodeWithTag(secondDestinationContentTag).assertExists()
171         assertWithMessage("First destination has not been disposed")
172             .that(firstDestinationCompositions).isEqualTo(0)
173         assertWithMessage("Second destination should have been composed exactly once")
174             .that(secondDestinationCompositions).isEqualTo(1)
175     }
176 
177     @Test
178     fun testBackStackEntryPoppedAfterManualSheetDismiss(): Unit = runBlocking {
179         val navigatorState = TestNavigatorState()
180         val sheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden, composeTestRule.density)
181         val navigator = BottomSheetNavigator(sheetState = sheetState)
182         navigator.onAttach(navigatorState)
183 
184         val bodyContentTag = "testBodyContent"
185 
186         composeTestRule.setContent {
187             ModalBottomSheetLayout(
188                 bottomSheetNavigator = navigator,
189                 content = {
190                     Box(
191                         Modifier
192                             .fillMaxSize()
193                             .testTag(bodyContentTag)
194                     )
195                 }
196             )
197         }
198 
199         val destination = BottomSheetNavigator.Destination(
200             navigator = navigator,
201             content = { Box(Modifier.height(20.dp)) }
202         )
203         val backStackEntry = navigatorState.createBackStackEntry(destination, null)
204         navigator.navigate(listOf(backStackEntry), null, null)
205         composeTestRule.awaitIdle()
206 
207         assertWithMessage("Navigated to destination")
208             .that(navigatorState.backStack.value)
209             .containsExactly(backStackEntry)
210         assertWithMessage("Bottom sheet shown")
211             .that(sheetState.isVisible).isTrue()
212 
213         composeTestRule.onNodeWithTag(bodyContentTag).performClick()
214         composeTestRule.awaitIdle()
215         assertWithMessage("Sheet should be hidden")
216             .that(sheetState.isVisible).isFalse()
217         assertThat(navigatorState.transitionsInProgress.value).isEmpty()
218         assertWithMessage("Back stack entry should be popped off the back stack")
219             .that(navigatorState.backStack.value)
220             .isEmpty()
221     }
222 
223     @Test
224     fun testSheetShownAfterNavControllerRestoresState() = runBlocking {
225         lateinit var navController: NavHostController
226         lateinit var navigator: BottomSheetNavigator
227         var savedState: Bundle? = null
228         var compositionState by mutableStateOf(0)
229 
230         val sheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden, composeTestRule.density)
231         val textInSheetTag = "textInSheet"
232 
233         composeTestRule.setContent {
234             navigator = remember { BottomSheetNavigator(sheetState) }
235             navController = rememberNavController(navigator)
236             if (savedState != null) navController.restoreState(savedState)
237             if (compositionState == 0) {
238                 ModalBottomSheetLayout(
239                     bottomSheetNavigator = navigator
240                 ) {
241                     NavHost(navController, startDestination = "first") {
242                         bottomSheet("first") {
243                             Text("Hello!", Modifier.testTag(textInSheetTag))
244                         }
245                     }
246                 }
247             }
248         }
249 
250         savedState = navController.saveState()
251 
252         // Dispose the ModalBottomSheetLayout
253         compositionState = 1
254         composeTestRule.awaitIdle()
255 
256         composeTestRule.onNodeWithTag(textInSheetTag).assertDoesNotExist()
257 
258         // Recompose with the ModalBottomSheetLayout
259         compositionState = 0
260         composeTestRule.awaitIdle()
261 
262         assertWithMessage("Destination is first destination")
263             .that(navController.currentDestination?.route)
264             .isEqualTo("first")
265         assertWithMessage("Bottom sheet is visible")
266             .that(sheetState.isVisible).isTrue()
267     }
268 
269     @Test
270     fun testNavigateCompletesEntriesTransitions() = runBlocking {
271         val sheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden, composeTestRule.density)
272         val navigator = BottomSheetNavigator(sheetState)
273         val navigatorState = TestNavigatorState()
274 
275         navigator.onAttach(navigatorState)
276 
277         composeTestRule.setContent {
278             ModalBottomSheetLayout(
279                 bottomSheetNavigator = navigator,
280                 content = { Box(Modifier.fillMaxSize()) }
281             )
282         }
283 
284         val backStackEntry1 = navigatorState.createBackStackEntry(
285             navigator.createFakeDestination(), null
286         )
287         val backStackEntry2 = navigatorState.createBackStackEntry(
288             navigator.createFakeDestination(), null
289         )
290 
291         navigator.navigate(
292             entries = listOf(backStackEntry1, backStackEntry2),
293             navOptions = null,
294             navigatorExtras = null
295         )
296 
297         composeTestRule.awaitIdle()
298 
299         assertThat(navigatorState.transitionsInProgress.value).doesNotContain(backStackEntry1)
300         assertThat(navigatorState.transitionsInProgress.value).doesNotContain(backStackEntry2)
301         assertThat(backStackEntry2.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
302     }
303 
304     @Test
305     fun testComposeSheetContentBeforeNavigatorAttached(): Unit = runBlocking {
306         val sheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden, composeTestRule.density)
307         val navigator = BottomSheetNavigator(sheetState)
308         val navigatorState = TestNavigatorState()
309 
310         composeTestRule.setContent {
311             ModalBottomSheetLayout(
312                 bottomSheetNavigator = navigator,
313                 content = { Box(Modifier.fillMaxSize()) }
314             )
315         }
316 
317         // Attach the state only after accessing the navigator's sheetContent in
318         // ModalBottomSheetLayout
319         navigator.onAttach(navigatorState)
320 
321         val entry = navigatorState.createBackStackEntry(
322             navigator.createFakeDestination(), null
323         )
324 
325         navigator.navigate(
326             entries = listOf(entry),
327             navOptions = null,
328             navigatorExtras = null
329         )
330 
331         composeTestRule.awaitIdle()
332 
333         assertWithMessage("The back stack entry has been added to the back stack")
334             .that(navigatorState.backStack.value)
335             .containsExactly(entry)
336     }
337 
338     @Test
339     fun testBackPressedDestroysEntry() {
340         lateinit var onBackPressedDispatcher: OnBackPressedDispatcher
341         lateinit var navController: NavHostController
342 
343         composeTestRule.setContent {
344             val bottomSheetNavigator = rememberBottomSheetNavigator()
345             navController = rememberNavController(bottomSheetNavigator)
346             onBackPressedDispatcher =
347                 LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher!!
348 
349             ModalBottomSheetLayout(bottomSheetNavigator) {
350                 Box(modifier = Modifier.fillMaxSize()) {
351                     NavHost(
352                         navController = navController,
353                         startDestination = "mainScreen"
354                     ) {
355 
356                         composable(
357                             route = "mainScreen",
358                             content = {
359                                 Button(onClick = { navController.navigate("bottomSheet") }) {
360                                     Text(text = "open drawer")
361                                 }
362                             }
363                         )
364 
365                         bottomSheet(
366                             route = "bottomSheet",
367                             content = {
368                                 Box(modifier = Modifier.fillMaxSize()) {
369                                     Text(
370                                         text = "bottomSheet"
371                                     )
372                                 }
373                             }
374                         )
375                     }
376                 }
377             }
378         }
379 
380         composeTestRule.onNodeWithText("open drawer").performClick()
381 
382         lateinit var bottomSheetEntry: NavBackStackEntry
383 
384         composeTestRule.runOnIdle {
385             bottomSheetEntry = navController.currentBackStackEntry!!
386             onBackPressedDispatcher.onBackPressed()
387         }
388 
389         composeTestRule.runOnIdle {
390             assertThat(bottomSheetEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED)
391         }
392     }
393 
394     @Test
395     fun testSheetContentSizeChangeDuringAnimation_opensSheet_shortSheetToShortSheet() {
396         lateinit var navigator: BottomSheetNavigator
397         lateinit var navController: NavHostController
398         var height: Dp by mutableStateOf(20.dp)
399         lateinit var sheetNavBackStackEntry: NavBackStackEntry
400         val homeDestination = "home"
401         val sheetDestination = "sheet"
402 
403         composeTestRule.setContent {
404             navigator = rememberBottomSheetNavigator()
405             navController = rememberNavController(navigator)
406             ModalBottomSheetLayout(navigator) {
407                 NavHost(navController, homeDestination) {
408                     composable(homeDestination) {
409                         Box(
410                             Modifier
411                                 .fillMaxSize()
412                                 .background(Color.Blue)
413                         )
414                     }
415                     bottomSheet(sheetDestination) { backStackEntry ->
416                         sheetNavBackStackEntry = backStackEntry
417                         Box(
418                             Modifier
419                                 .height(height)
420                                 .fillMaxWidth()
421                                 .background(Color.Red)
422                         )
423                     }
424                 }
425             }
426         }
427 
428         composeTestRule.mainClock.autoAdvance = false
429         composeTestRule.runOnUiThread { navController.navigate(sheetDestination) }
430         composeTestRule.mainClock.advanceTimeBy(100)
431 
432         assertThat(navigator.transitionsInProgress.value.lastOrNull())
433             .isEqualTo(sheetNavBackStackEntry)
434 
435         height = (composeTestRule.onRoot().getUnclippedBoundsInRoot().height) / 3
436 
437         composeTestRule.mainClock.autoAdvance = true
438         composeTestRule.waitForIdle()
439         assertThat(navigator.navigatorSheetState.isVisible).isTrue()
440 
441         assertThat(navigator.transitionsInProgress.value).isEmpty()
442 
443         composeTestRule.runOnUiThread { navController.navigate(homeDestination) }
444         composeTestRule.waitForIdle()
445         assertThat(navigator.navigatorSheetState.isVisible).isFalse()
446     }
447 
448     @Test
449     fun testSheetContentSizeChangeDuringAnimation_opensSheet_shortSheetToTallSheet() {
450         lateinit var navigator: BottomSheetNavigator
451         lateinit var navController: NavHostController
452         var height: Dp by mutableStateOf(20.dp)
453         lateinit var sheetNavBackStackEntry: NavBackStackEntry
454         val homeDestination = "home"
455         val sheetDestination = "sheet"
456 
457         composeTestRule.setContent {
458             navigator = rememberBottomSheetNavigator()
459             navController = rememberNavController(navigator)
460             ModalBottomSheetLayout(navigator) {
461                 NavHost(navController, homeDestination) {
462                     composable(homeDestination) {
463                         Box(Modifier.fillMaxSize().background(Color.Blue))
464                     }
465                     bottomSheet(sheetDestination) { backStackEntry ->
466                         sheetNavBackStackEntry = backStackEntry
467                         Box(Modifier.height(height).fillMaxWidth().background(Color.Red))
468                     }
469                 }
470             }
471         }
472 
473         composeTestRule.mainClock.autoAdvance = false
474         composeTestRule.runOnUiThread { navController.navigate(sheetDestination) }
475         composeTestRule.mainClock.advanceTimeBy(100)
476         assertThat(navigator.transitionsInProgress.value.lastOrNull())
477             .isEqualTo(sheetNavBackStackEntry)
478 
479         height = (composeTestRule.onRoot().getUnclippedBoundsInRoot().height) / 0.9f
480 
481         composeTestRule.mainClock.autoAdvance = true
482         composeTestRule.waitForIdle()
483         assertThat(navigator.navigatorSheetState.isVisible).isTrue()
484 
485         assertThat(navigator.transitionsInProgress.value).isEmpty()
486 
487         composeTestRule.runOnUiThread { navController.navigate(homeDestination) }
488         composeTestRule.waitForIdle()
489         assertThat(navigator.navigatorSheetState.isVisible).isFalse()
490     }
491 
492     @Test
493     fun testSheetContentSizeChangeDuringAnimation_opensSheet_tallSheetToTallSheet() {
494         lateinit var navigator: BottomSheetNavigator
495         lateinit var navController: NavHostController
496         lateinit var sheetNavBackStackEntry: NavBackStackEntry
497         var height: Dp by mutableStateOf(0.dp)
498         val homeDestination = "home"
499         val sheetDestination = "sheet"
500 
501         composeTestRule.setContent {
502             navigator = rememberBottomSheetNavigator()
503             navController = rememberNavController(navigator)
504             ModalBottomSheetLayout(navigator) {
505                 NavHost(navController, homeDestination) {
506                     composable(homeDestination) {
507                         Box(Modifier.fillMaxSize().background(Color.Blue))
508                     }
509                     bottomSheet(sheetDestination) { backStackEntry ->
510                         sheetNavBackStackEntry = backStackEntry
511                         Box(Modifier.height(height).fillMaxWidth().background(Color.Red))
512                     }
513                 }
514             }
515         }
516 
517         val rootHeight = composeTestRule.onRoot().getUnclippedBoundsInRoot().height
518         height = rootHeight
519 
520         composeTestRule.mainClock.autoAdvance = false
521         composeTestRule.runOnUiThread { navController.navigate(sheetDestination) }
522         composeTestRule.mainClock.advanceTimeBy(100)
523         assertThat(navigator.transitionsInProgress.value.lastOrNull())
524             .isEqualTo(sheetNavBackStackEntry)
525 
526         height = (composeTestRule.onRoot().getUnclippedBoundsInRoot().height) / 0.9f
527 
528         composeTestRule.mainClock.autoAdvance = true
529         composeTestRule.waitForIdle()
530         assertThat(navigator.navigatorSheetState.isVisible).isTrue()
531 
532         assertThat(navigator.transitionsInProgress.value).isEmpty()
533 
534         composeTestRule.runOnUiThread { navController.navigate(homeDestination) }
535         composeTestRule.waitForIdle()
536         assertThat(navigator.navigatorSheetState.isVisible).isFalse()
537     }
538 
539     @Test
540     fun testSheetContentSizeChangeDuringAnimation_opensSheet_tallSheetToShortSheet() {
541         lateinit var navigator: BottomSheetNavigator
542         lateinit var navController: NavHostController
543         var height: Dp by mutableStateOf(0.dp)
544         lateinit var sheetNavBackStackEntry: NavBackStackEntry
545         val homeDestination = "home"
546         val sheetDestination = "sheet"
547 
548         composeTestRule.setContent {
549             navigator = rememberBottomSheetNavigator()
550             navController = rememberNavController(navigator)
551             ModalBottomSheetLayout(navigator) {
552                 NavHost(navController, homeDestination) {
553                     composable(homeDestination) {
554                         Box(Modifier.fillMaxSize().background(Color.Blue))
555                     }
556                     bottomSheet(sheetDestination) { backStackEntry ->
557                         sheetNavBackStackEntry = backStackEntry
558                         Box(Modifier.height(height).fillMaxWidth().background(Color.Red))
559                     }
560                 }
561             }
562         }
563 
564         val rootHeight = composeTestRule.onRoot().getUnclippedBoundsInRoot().height
565         height = rootHeight
566 
567         composeTestRule.mainClock.autoAdvance = false
568         composeTestRule.runOnUiThread { navController.navigate(sheetDestination) }
569         composeTestRule.mainClock.advanceTimeBy(100)
570         assertThat(navigator.transitionsInProgress.value.lastOrNull())
571             .isEqualTo(sheetNavBackStackEntry)
572 
573         height = (composeTestRule.onRoot().getUnclippedBoundsInRoot().height) / 3f
574 
575         composeTestRule.mainClock.autoAdvance = true
576         composeTestRule.waitForIdle()
577         assertThat(navigator.navigatorSheetState.isVisible).isTrue()
578 
579         assertThat(navigator.transitionsInProgress.value).isEmpty()
580 
581         composeTestRule.runOnUiThread { navController.navigate(homeDestination) }
582         composeTestRule.waitForIdle()
583         assertThat(navigator.navigatorSheetState.isVisible).isFalse()
584     }
585 
586     @OptIn(ExperimentalMaterialApi::class)
587     @Test
588     fun testPopBackStackHidesSheetWithAnimation() {
589         val animationDuration = 2000
590         val animationSpec = tween<Float>(animationDuration)
591         lateinit var navigator: BottomSheetNavigator
592         lateinit var navController: NavHostController
593 
594         composeTestRule.setContent {
595             navigator = rememberBottomSheetNavigator(animationSpec)
596             navController = rememberNavController(navigator)
597             ModalBottomSheetLayout(navigator) {
598                 NavHost(navController, "first") {
599                     composable("first") {
600                         Box(Modifier.fillMaxSize())
601                     }
602                     bottomSheet("sheet") {
603                         Box(Modifier.height(200.dp))
604                     }
605                 }
606             }
607         }
608 
609         composeTestRule.runOnUiThread { navController.navigate("sheet") }
610         composeTestRule.waitForIdle()
611         assertThat(navigator.navigatorSheetState.isVisible).isTrue()
612 
613         composeTestRule.mainClock.autoAdvance = false
614         composeTestRule.runOnUiThread { navController.popBackStack() }
615 
616         val firstAnimationTimeBreakpoint = (animationDuration * 0.9).roundToLong()
617 
618         composeTestRule.mainClock.advanceTimeBy(firstAnimationTimeBreakpoint)
619         assertThat(navigator.navigatorSheetState.currentValue)
620             .isAnyOf(ModalBottomSheetValue.HalfExpanded, ModalBottomSheetValue.Expanded)
621         assertThat(navigator.navigatorSheetState.targetValue)
622             .isEqualTo(ModalBottomSheetValue.Hidden)
623 
624         composeTestRule.runOnUiThread { navController.navigate("first") }
625 
626         composeTestRule.mainClock.autoAdvance = true
627         composeTestRule.waitForIdle()
628         assertThat(navigator.navigatorSheetState.currentValue)
629             .isEqualTo(ModalBottomSheetValue.Hidden)
630     }
631 
632     @Test
633     fun testTapOnScrimDismissesSheetAndPopsBackStack() {
634         val animationDuration = 2000
635         val animationSpec = tween<Float>(animationDuration)
636         lateinit var navigator: BottomSheetNavigator
637         lateinit var navController: NavHostController
638         val sheetLayoutTestTag = "sheetLayout"
639         val homeDestination = "home"
640         val sheetDestination = "sheet"
641 
642         composeTestRule.setContent {
643             navigator = rememberBottomSheetNavigator(animationSpec)
644             navController = rememberNavController(navigator)
645             ModalBottomSheetLayout(navigator, Modifier.testTag(sheetLayoutTestTag)) {
646                 NavHost(navController, homeDestination) {
647                     composable(homeDestination) {
648                         Box(
649                             Modifier
650                                 .fillMaxSize()
651                                 .background(Color.Red)
652                         )
653                     }
654                     bottomSheet(sheetDestination) {
655                         Box(
656                             Modifier
657                                 .height(200.dp)
658                                 .fillMaxWidth()
659                                 .background(Color.Green)
660                         ) {
661                             Text("Hello!")
662                         }
663                     }
664                 }
665             }
666         }
667 
668         assertThat(navController.currentBackStackEntry?.destination?.route).isEqualTo(
669             homeDestination
670         )
671         assertThat(navigator.navigatorSheetState.isVisible).isFalse()
672 
673         composeTestRule.runOnUiThread { navController.navigate(sheetDestination) }
674         composeTestRule.waitForIdle()
675 
676         assertThat(navController.currentBackStackEntry?.destination?.route).isEqualTo(
677             sheetDestination
678         )
679         assertThat(navController.currentBackStackEntry?.lifecycle?.currentState).isEqualTo(Lifecycle.State.RESUMED)
680         assertThat(navigator.navigatorSheetState.isVisible).isTrue()
681 
682         composeTestRule.onNodeWithTag(sheetLayoutTestTag)
683             .performTouchInput { click(position = topCenter) }
684 
685         composeTestRule.waitForIdle()
686         assertThat(navigator.navigatorSheetState.isVisible).isFalse()
687     }
688 
689     @Test
690     fun testNavigatingFromSheetToSheetDismissesAndThenShowsSheet() {
691         val animationDuration = 2000
692         val animationSpec = tween<Float>(animationDuration)
693         lateinit var navigator: BottomSheetNavigator
694         lateinit var navController: NavHostController
695         val sheetLayoutTestTag = "sheetLayout"
696         val homeDestination = "home"
697         val firstSheetDestination = "sheet1"
698         val secondSheetDestination = "sheet2"
699 
700         composeTestRule.setContent {
701             navigator = rememberBottomSheetNavigator(animationSpec)
702             navController = rememberNavController(navigator)
703             ModalBottomSheetLayout(navigator, Modifier.testTag(sheetLayoutTestTag)) {
704                 NavHost(navController, homeDestination) {
705                     composable(homeDestination) {
706                         Box(
707                             Modifier
708                                 .fillMaxSize()
709                                 .background(Color.Red)
710                         )
711                     }
712                     bottomSheet(firstSheetDestination) {
713                         Box(
714                             Modifier
715                                 .height(200.dp)
716                                 .fillMaxWidth()
717                                 .background(Color.Green)
718                         ) {
719                             Text("Hello!")
720                         }
721                     }
722                     bottomSheet(secondSheetDestination) {
723                         Box(
724                             Modifier
725                                 .height(200.dp)
726                                 .fillMaxWidth()
727                                 .background(Color.Blue)
728                         ) {
729                             Text("Hello!")
730                         }
731                     }
732                 }
733             }
734         }
735 
736         assertThat(navController.currentBackStackEntry?.destination?.route)
737             .isEqualTo(homeDestination)
738 
739         composeTestRule.runOnUiThread { navController.navigate(firstSheetDestination) }
740         composeTestRule.waitForIdle()
741 
742         assertThat(navController.currentBackStackEntry?.destination?.route)
743             .isEqualTo(firstSheetDestination)
744         assertThat(navigator.sheetState.currentValue)
745             .isAnyOf(ModalBottomSheetValue.HalfExpanded, ModalBottomSheetValue.Expanded)
746 
747         composeTestRule.mainClock.autoAdvance = false
748         composeTestRule.runOnUiThread { navController.navigate(secondSheetDestination) }
749 
750         composeTestRule.mainClock.advanceTimeUntil { navigator.sheetState.isAnimationRunning }
751         composeTestRule.mainClock.advanceTimeBy(animationDuration.toLong())
752         composeTestRule.mainClock.advanceTimeByFrame()
753 
754         assertThat(navigator.sheetState.currentValue).isEqualTo(ModalBottomSheetValue.Hidden)
755 
756         composeTestRule.mainClock.advanceTimeUntil { navigator.sheetState.isAnimationRunning }
757         composeTestRule.mainClock.advanceTimeBy(animationDuration.toLong())
758         composeTestRule.mainClock.advanceTimeByFrame()
759 
760         assertThat(navigator.sheetState.currentValue)
761             .isAnyOf(ModalBottomSheetValue.HalfExpanded, ModalBottomSheetValue.Expanded)
762         assertThat(navController.currentBackStackEntry?.destination?.route)
763             .isEqualTo(secondSheetDestination)
764 
765         composeTestRule.runOnUiThread {
766             navController.popBackStack(firstSheetDestination, inclusive = false)
767         }
768         composeTestRule.mainClock.advanceTimeBy(animationDuration.toLong())
769         composeTestRule.mainClock.advanceTimeByFrame()
770 
771         assertThat(navigator.sheetState.currentValue).isEqualTo(ModalBottomSheetValue.Hidden)
772 
773         composeTestRule.mainClock.autoAdvance = true
774         composeTestRule.waitForIdle()
775 
776         assertThat(navigator.sheetState.currentValue)
777             .isAnyOf(ModalBottomSheetValue.HalfExpanded, ModalBottomSheetValue.Expanded)
778         assertThat(navController.currentBackStackEntry?.destination?.route)
779             .isEqualTo(firstSheetDestination)
780     }
781 
782     @Test
783     fun testBackPressWithNestedGraphBehind() {
784         lateinit var navigator: BottomSheetNavigator
785         lateinit var navController: NavHostController
786         lateinit var nestedNavController: NavHostController
787         lateinit var backDispatcher: OnBackPressedDispatcher
788         val homeDestination = "home"
789         val firstSheetDestination = "sheet1"
790         val firstNestedDestination = "nested1"
791         val secondNestedDestination = "nested2"
792 
793         composeTestRule.setContent {
794             backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher!!
795             navigator = rememberBottomSheetNavigator()
796             navController = rememberNavController(navigator)
797             ModalBottomSheetLayout(navigator) {
798                 NavHost(navController, homeDestination) {
799                     composable(homeDestination) {
800                         nestedNavController = rememberNavController()
801                         NavHost(nestedNavController, "nested1") {
802                             composable(firstNestedDestination) { }
803                             composable(secondNestedDestination) { }
804                         }
805                     }
806                     bottomSheet(firstSheetDestination) {
807                         Text("SheetDestination")
808                     }
809                 }
810             }
811         }
812 
813         assertThat(navController.currentBackStackEntry?.destination?.route)
814             .isEqualTo(homeDestination)
815 
816         composeTestRule.runOnUiThread {
817             nestedNavController.navigate(secondNestedDestination)
818         }
819         composeTestRule.waitForIdle()
820 
821         assertThat(navController.currentBackStackEntry?.destination?.route)
822             .isEqualTo(homeDestination)
823         assertThat(nestedNavController.currentBackStackEntry?.destination?.route)
824             .isEqualTo(secondNestedDestination)
825 
826         composeTestRule.runOnUiThread {
827             navController.navigate(firstSheetDestination)
828         }
829         composeTestRule.waitForIdle()
830 
831         assertThat(navigator.sheetState.currentValue)
832             .isAnyOf(ModalBottomSheetValue.HalfExpanded, ModalBottomSheetValue.Expanded)
833 
834         composeTestRule.runOnUiThread {
835             backDispatcher.onBackPressed()
836         }
837         composeTestRule.waitForIdle()
838 
839         assertThat(navController.currentBackStackEntry?.destination?.route)
840             .isEqualTo(homeDestination)
841         assertThat(nestedNavController.currentBackStackEntry?.destination?.route)
842             .isEqualTo(secondNestedDestination)
843 
844         assertThat(navigator.sheetState.currentValue).isEqualTo(ModalBottomSheetValue.Hidden)
845     }
846 
847     private fun BottomSheetNavigator.createFakeDestination() =
848         BottomSheetNavigator.Destination(this) {
849             Text("Fake Sheet Content")
850         }
851 
852     private val ModalBottomSheetState.isAnimationRunning get() = currentValue != targetValue
853 }
854