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