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 * http://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 androidx.navigation.testing 18 19 import androidx.kruth.assertThat 20 import androidx.lifecycle.Lifecycle 21 import androidx.lifecycle.LifecycleEventObserver 22 import androidx.lifecycle.LifecycleOwner 23 import androidx.lifecycle.ViewModel 24 import androidx.lifecycle.ViewModelProvider 25 import androidx.lifecycle.testing.TestLifecycleOwner 26 import androidx.navigation.FloatingWindow 27 import androidx.navigation.NavBackStackEntry 28 import androidx.navigation.NavDestination 29 import androidx.navigation.NavOptions 30 import androidx.navigation.Navigator 31 import androidx.navigation.SupportingPane 32 import androidx.navigation.navOptions 33 import androidx.test.ext.junit.runners.AndroidJUnit4 34 import androidx.test.filters.SmallTest 35 import org.junit.Before 36 import org.junit.Test 37 import org.junit.runner.RunWith 38 39 @SmallTest 40 @RunWith(AndroidJUnit4::class) 41 class TestNavigatorStateTest { 42 private lateinit var state: TestNavigatorState 43 44 @Before 45 fun setUp() { 46 state = TestNavigatorState() 47 } 48 49 @Test 50 fun testLifecycle() { 51 val navigator = TestNavigator() 52 navigator.onAttach(state) 53 val firstEntry = state.createBackStackEntry(navigator.createDestination(), null) 54 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED) 55 56 navigator.navigate(listOf(firstEntry), null, null) 57 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 58 59 val secondEntry = state.createBackStackEntry(navigator.createDestination(), null) 60 navigator.navigate(listOf(secondEntry), null, null) 61 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED) 62 assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 63 64 navigator.popBackStack(secondEntry, false) 65 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 66 assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED) 67 } 68 69 @Test 70 fun testFloatingWindowLifecycle() { 71 val navigator = FloatingWindowTestNavigator() 72 navigator.onAttach(state) 73 val firstEntry = state.createBackStackEntry(navigator.createDestination(), null) 74 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED) 75 76 navigator.navigate(listOf(firstEntry), null, null) 77 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 78 79 val secondEntry = state.createBackStackEntry(navigator.createDestination(), null) 80 navigator.navigate(listOf(secondEntry), null, null) 81 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) 82 assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 83 84 navigator.popBackStack(secondEntry, false) 85 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 86 assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED) 87 } 88 89 @Test 90 fun testSupportingPaneLifecycle() { 91 val navigator = SupportingPaneTestNavigator() 92 navigator.onAttach(state) 93 val firstEntry = state.createBackStackEntry(navigator.createDestination(), null) 94 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED) 95 96 navigator.navigate(listOf(firstEntry), null, null) 97 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 98 99 val secondEntry = state.createBackStackEntry(navigator.createDestination(), null) 100 navigator.navigate(listOf(secondEntry), null, null) 101 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 102 assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 103 104 navigator.popBackStack(secondEntry, false) 105 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 106 assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED) 107 } 108 109 @Test 110 fun testWithTransitionLifecycle() { 111 val navigator = TestTransitionNavigator() 112 navigator.onAttach(state) 113 val firstEntry = state.createBackStackEntry(navigator.createDestination(), null) 114 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED) 115 116 navigator.navigate(listOf(firstEntry), null, null) 117 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) 118 119 state.markTransitionComplete(firstEntry) 120 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 121 122 val secondEntry = state.createBackStackEntry(navigator.createDestination(), null) 123 navigator.navigate(listOf(secondEntry), null, null) 124 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED) 125 assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) 126 assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue() 127 128 state.markTransitionComplete(firstEntry) 129 state.markTransitionComplete(secondEntry) 130 assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 131 132 navigator.popBackStack(secondEntry, true) 133 assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED) 134 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) 135 136 state.markTransitionComplete(firstEntry) 137 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 138 state.markTransitionComplete(secondEntry) 139 assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED) 140 141 val restoredSecondEntry = state.restoreBackStackEntry(secondEntry) 142 navigator.navigate(listOf(restoredSecondEntry), null, null) 143 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED) 144 assertThat(restoredSecondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) 145 assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue() 146 147 state.markTransitionComplete(firstEntry) 148 state.markTransitionComplete(restoredSecondEntry) 149 assertThat(restoredSecondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 150 } 151 152 @Test 153 fun testWithSupportingPaneTransitionLifecycle() { 154 val navigator = SupportingPaneTestTransitionNavigator() 155 navigator.onAttach(state) 156 val firstEntry = state.createBackStackEntry(navigator.createDestination(), null) 157 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED) 158 159 navigator.navigate(listOf(firstEntry), null, null) 160 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) 161 162 state.markTransitionComplete(firstEntry) 163 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 164 165 val secondEntry = state.createBackStackEntry(navigator.createDestination(), null) 166 navigator.navigate(listOf(secondEntry), null, null) 167 // Both are started because they are SupportingPane destinations 168 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) 169 assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) 170 assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue() 171 172 state.markTransitionComplete(secondEntry) 173 // Even though the secondEntry has completed its transition, the firstEntry 174 // hasn't completed its transition, so it shouldn't be resumed yet 175 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) 176 assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 177 178 state.markTransitionComplete(firstEntry) 179 // Both are resumed because they are SupportingPane destinations that have finished 180 // their transitions 181 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 182 assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 183 184 navigator.popBackStack(secondEntry, true) 185 assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED) 186 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) 187 188 state.markTransitionComplete(firstEntry) 189 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 190 state.markTransitionComplete(secondEntry) 191 assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED) 192 193 val restoredSecondEntry = state.restoreBackStackEntry(secondEntry) 194 navigator.navigate(listOf(restoredSecondEntry), null, null) 195 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) 196 assertThat(restoredSecondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) 197 assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue() 198 199 state.markTransitionComplete(firstEntry) 200 state.markTransitionComplete(restoredSecondEntry) 201 assertThat(restoredSecondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 202 } 203 204 @Test 205 fun testSameEntry() { 206 val navigator = TestTransitionNavigator() 207 navigator.onAttach(state) 208 val firstEntry = state.createBackStackEntry(navigator.createDestination(), null) 209 210 navigator.navigate(listOf(firstEntry), null, null) 211 state.markTransitionComplete(firstEntry) 212 213 val secondEntry = state.createBackStackEntry(navigator.createDestination(), null) 214 navigator.navigate(listOf(secondEntry), null, null) 215 assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue() 216 217 state.markTransitionComplete(firstEntry) 218 state.markTransitionComplete(secondEntry) 219 220 navigator.popBackStack(secondEntry, true) 221 assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue() 222 assertThat(state.transitionsInProgress.value.contains(secondEntry)).isTrue() 223 state.markTransitionComplete(firstEntry) 224 state.markTransitionComplete(secondEntry) 225 val restoredSecondEntry = state.restoreBackStackEntry(secondEntry) 226 navigator.navigate(listOf(restoredSecondEntry), null, null) 227 assertThat(state.transitionsInProgress.value.firstOrNull { it === restoredSecondEntry }) 228 .isNotNull() 229 230 state.markTransitionComplete(firstEntry) 231 assertThat(state.transitionsInProgress.value.contains(firstEntry)).isFalse() 232 233 state.markTransitionComplete(restoredSecondEntry) 234 assertThat(state.transitionsInProgress.value.firstOrNull { it === secondEntry }).isNull() 235 236 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED) 237 assertThat(restoredSecondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 238 239 state.markTransitionComplete(secondEntry) 240 assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED) 241 } 242 243 @Test 244 fun testNewInstanceBeforeComplete() { 245 val navigator = TestTransitionNavigator() 246 navigator.onAttach(state) 247 val firstEntry = state.createBackStackEntry(navigator.createDestination(), null) 248 firstEntry.destination.route = "first" 249 250 navigator.navigate(listOf(firstEntry), null, null) 251 state.markTransitionComplete(firstEntry) 252 253 val secondEntry = state.createBackStackEntry(navigator.createDestination(), null) 254 secondEntry.destination.route = "second" 255 navigator.navigate( 256 listOf(secondEntry), 257 navOptions { 258 popUpTo("first") { saveState = true } 259 launchSingleTop = true 260 restoreState = true 261 }, 262 null 263 ) 264 265 val viewModel = ViewModelProvider(secondEntry).get(TestViewModel::class.java) 266 267 navigator.popBackStack(secondEntry, true) 268 val restoredSecondEntry = state.restoreBackStackEntry(secondEntry) 269 navigator.navigate( 270 listOf(restoredSecondEntry), 271 navOptions { 272 popUpTo("first") { saveState = true } 273 launchSingleTop = true 274 restoreState = true 275 }, 276 null 277 ) 278 279 state.transitionsInProgress.value.forEach { state.markTransitionComplete(it) } 280 281 assertThat(viewModel.wasCleared).isFalse() 282 } 283 284 @Test 285 fun testTransitionInterruptPushPop() { 286 val navigator = TestTransitionNavigator() 287 navigator.onAttach(state) 288 val firstEntry = state.createBackStackEntry(navigator.createDestination(), null) 289 290 navigator.navigate(listOf(firstEntry), null, null) 291 state.markTransitionComplete(firstEntry) 292 293 val secondEntry = state.createBackStackEntry(navigator.createDestination(), null) 294 navigator.navigate(listOf(secondEntry), null, null) 295 assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue() 296 assertThat(state.transitionsInProgress.value.contains(secondEntry)).isTrue() 297 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED) 298 assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) 299 300 navigator.popBackStack(secondEntry, true) 301 assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue() 302 assertThat(state.transitionsInProgress.value.contains(secondEntry)).isTrue() 303 state.markTransitionComplete(firstEntry) 304 state.markTransitionComplete(secondEntry) 305 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 306 assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED) 307 } 308 309 @Test 310 fun testTransitionInterruptPopPush() { 311 val navigator = TestTransitionNavigator() 312 navigator.onAttach(state) 313 val firstEntry = state.createBackStackEntry(navigator.createDestination(), null) 314 315 navigator.navigate(listOf(firstEntry), null, null) 316 state.markTransitionComplete(firstEntry) 317 318 val secondEntry = state.createBackStackEntry(navigator.createDestination(), null) 319 navigator.navigate(listOf(secondEntry), null, null) 320 assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue() 321 assertThat(state.transitionsInProgress.value.contains(secondEntry)).isTrue() 322 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED) 323 assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) 324 state.markTransitionComplete(firstEntry) 325 state.markTransitionComplete(secondEntry) 326 327 navigator.popBackStack(secondEntry, true) 328 assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue() 329 assertThat(state.transitionsInProgress.value.contains(secondEntry)).isTrue() 330 331 val secondEntryReplace = state.createBackStackEntry(navigator.createDestination(), null) 332 navigator.navigate(listOf(secondEntryReplace), null, null) 333 assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue() 334 assertThat(state.transitionsInProgress.value.contains(secondEntry)).isTrue() 335 assertThat(state.transitionsInProgress.value.contains(secondEntryReplace)).isTrue() 336 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED) 337 assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED) 338 assertThat(secondEntryReplace.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) 339 state.markTransitionComplete(firstEntry) 340 state.markTransitionComplete(secondEntry) 341 state.markTransitionComplete(secondEntryReplace) 342 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED) 343 assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED) 344 assertThat(secondEntryReplace.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 345 } 346 347 @Test 348 fun testPrepareForTransition() { 349 val navigator = TestTransitionNavigator() 350 navigator.onAttach(state) 351 val firstEntry = state.createBackStackEntry(navigator.createDestination(), null) 352 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED) 353 354 navigator.navigate(listOf(firstEntry), null, null) 355 navigator.testLifecycle.addObserver( 356 object : LifecycleEventObserver { 357 override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { 358 when (event) { 359 Lifecycle.Event.ON_STOP -> { 360 // this is okay since the first entry will not be DESTROYED in this 361 // test. 362 state.markTransitionComplete(firstEntry) 363 } 364 Lifecycle.Event.ON_PAUSE -> state.prepareForTransition(firstEntry) 365 Lifecycle.Event.ON_START -> state.prepareForTransition(firstEntry) 366 Lifecycle.Event.ON_RESUME -> state.markTransitionComplete(firstEntry) 367 else -> {} 368 } 369 } 370 } 371 ) 372 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) 373 374 navigator.testLifecycle.currentState = Lifecycle.State.RESUMED 375 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 376 377 navigator.testLifecycle.currentState = Lifecycle.State.STARTED 378 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) 379 380 val secondEntry = state.createBackStackEntry(navigator.createDestination(), null) 381 navigator.navigate(listOf(secondEntry), null, null) 382 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED) 383 assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) 384 assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue() 385 386 // Moving down to reflect a destination being on a back stack and only CREATED 387 navigator.testLifecycle.currentState = Lifecycle.State.CREATED 388 389 state.markTransitionComplete(secondEntry) 390 assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 391 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED) 392 393 navigator.testLifecycle.currentState = Lifecycle.State.STARTED 394 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) 395 396 navigator.popBackStack(secondEntry, true) 397 assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED) 398 399 navigator.testLifecycle.currentState = Lifecycle.State.RESUMED 400 assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) 401 state.markTransitionComplete(secondEntry) 402 assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED) 403 } 404 405 @Navigator.Name("test") 406 internal class TestNavigator : Navigator<NavDestination>() { 407 override fun createDestination(): NavDestination = NavDestination(this) 408 } 409 410 @Navigator.Name("test") 411 internal class TestTransitionNavigator : Navigator<NavDestination>() { 412 private val testLifecycleOwner = TestLifecycleOwner() 413 val testLifecycle = testLifecycleOwner.lifecycle 414 415 override fun createDestination(): NavDestination = NavDestination(this) 416 417 override fun navigate( 418 entries: List<NavBackStackEntry>, 419 navOptions: NavOptions?, 420 navigatorExtras: Extras? 421 ) { 422 entries.forEach { entry -> state.pushWithTransition(entry) } 423 } 424 425 override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) { 426 state.popWithTransition(popUpTo, savedState) 427 } 428 } 429 430 @Navigator.Name("test") 431 internal class FloatingWindowTestNavigator : Navigator<FloatingTestDestination>() { 432 override fun createDestination(): FloatingTestDestination = FloatingTestDestination(this) 433 } 434 435 internal class FloatingTestDestination(navigator: Navigator<out NavDestination>) : 436 NavDestination(navigator), FloatingWindow 437 438 @Navigator.Name("test") 439 internal class SupportingPaneTestNavigator : Navigator<SupportingPaneTestDestination>() { 440 override fun createDestination(): SupportingPaneTestDestination = 441 SupportingPaneTestDestination(this) 442 } 443 444 @Navigator.Name("test") 445 internal class SupportingPaneTestTransitionNavigator : 446 Navigator<SupportingPaneTestDestination>() { 447 448 override fun createDestination(): SupportingPaneTestDestination = 449 SupportingPaneTestDestination(this) 450 451 override fun navigate( 452 entries: List<NavBackStackEntry>, 453 navOptions: NavOptions?, 454 navigatorExtras: Extras? 455 ) { 456 entries.forEach { entry -> state.pushWithTransition(entry) } 457 } 458 459 override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) { 460 state.popWithTransition(popUpTo, savedState) 461 } 462 } 463 464 internal class SupportingPaneTestDestination(navigator: Navigator<out NavDestination>) : 465 NavDestination(navigator), SupportingPane 466 467 class TestViewModel : ViewModel() { 468 var wasCleared = false 469 470 override fun onCleared() { 471 super.onCleared() 472 wasCleared = true 473 } 474 } 475 } 476