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