DefaultSessionControlControllerTest.kt 21 KB
Newer Older
1
2
3
4
5
6
7
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.fenix.home

import androidx.navigation.NavController
8
import androidx.navigation.NavDirections
9
10
11
12
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
13
import kotlinx.coroutines.test.TestCoroutineDispatcher
14
import kotlinx.coroutines.test.TestCoroutineScope
15
import mozilla.components.browser.state.action.SearchAction
16
import mozilla.components.browser.state.action.TabListAction
17
import mozilla.components.browser.state.search.RegionState
18
19
import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.browser.state.state.BrowserState
20
import mozilla.components.browser.state.state.ReaderState
21
import mozilla.components.browser.state.state.SearchState
22
import mozilla.components.browser.state.state.createTab
23
import mozilla.components.browser.state.state.recover.RecoverableTab
24
import mozilla.components.browser.state.store.BrowserStore
25
import mozilla.components.concept.engine.Engine
26
import mozilla.components.feature.session.SessionUseCases
27
import mozilla.components.feature.tab.collections.TabCollection
28
import mozilla.components.feature.tabs.TabsUseCases
29
import mozilla.components.feature.top.sites.TopSite
30
import mozilla.components.support.test.ext.joinBlocking
31
import mozilla.components.support.test.rule.MainCoroutineRule
32
import org.junit.After
33
import org.junit.Before
34
import org.junit.Rule
35
36
37
import org.junit.Test
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
38
import org.mozilla.fenix.R
39
import org.mozilla.fenix.components.Analytics
40
import org.mozilla.fenix.components.TabCollectionStorage
41
42
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
43
import org.mozilla.fenix.components.tips.Tip
44
45
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
46
47
import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
import org.mozilla.fenix.settings.SupportUtils
48
import org.mozilla.fenix.utils.Settings
49
import mozilla.components.feature.tab.collections.Tab as ComponentTab
50

51
@OptIn(ExperimentalCoroutinesApi::class)
52
53
class DefaultSessionControlControllerTest {

54
55
    private val testDispatcher = TestCoroutineDispatcher()

56
    @get:Rule
57
    val coroutinesTestRule = MainCoroutineRule(testDispatcher)
58

59
    private val activity: HomeActivity = mockk(relaxed = true)
60
    private val fragmentStore: HomeFragmentStore = mockk(relaxed = true)
61
62
    private val navController: NavController = mockk(relaxed = true)
    private val metrics: MetricController = mockk(relaxed = true)
63
64
    private val engine: Engine = mockk(relaxed = true)
    private val tabCollectionStorage: TabCollectionStorage = mockk(relaxed = true)
65
    private val tabsUseCases: TabsUseCases = mockk(relaxed = true)
66
    private val reloadUrlUseCase: SessionUseCases = mockk(relaxed = true)
67
    private val selectTabUseCase: TabsUseCases = mockk(relaxed = true)
68
69
70
    private val hideOnboarding: () -> Unit = mockk(relaxed = true)
    private val registerCollectionStorageObserver: () -> Unit = mockk(relaxed = true)
    private val showTabTray: () -> Unit = mockk(relaxed = true)
71
72
73
74
75
76
77
78
    private val handleSwipedItemDeletionCancel: () -> Unit = mockk(relaxed = true)
    private val showDeleteCollectionPrompt: (
        tabCollection: TabCollection,
        title: String?,
        message: String,
        wasSwiped: Boolean,
        handleSwipedItemDeletionCancel: () -> Unit
    ) -> Unit = mockk(relaxed = true)
79
80
    private val settings: Settings = mockk(relaxed = true)
    private val analytics: Analytics = mockk(relaxed = true)
81
    private val scope = TestCoroutineScope()
82
83
84
85
86
87
88
89
    private val searchEngine = SearchEngine(
        id = "test",
        name = "Test Engine",
        icon = mockk(relaxed = true),
        type = SearchEngine.Type.BUNDLED,
        resultUrls = listOf("https://example.org/?q={searchTerms}")
    )
    private lateinit var store: BrowserStore
90
91
92
93
    private lateinit var controller: DefaultSessionControlController

    @Before
    fun setup() {
94
95
96
97
98
        store = BrowserStore(
            BrowserState(
                search = SearchState(
                    regionSearchEngines = listOf(searchEngine)
                )
99
            )
100
        )
101

102
103
104
105
        every { fragmentStore.state } returns HomeFragmentState(
            collections = emptyList(),
            expandedCollections = emptySet(),
            mode = Mode.Normal,
106
107
            topSites = emptyList(),
            showCollectionPlaceholder = true
108
        )
109

110
111
112
        every { navController.currentDestination } returns mockk {
            every { id } returns R.id.homeFragment
        }
113
114
115
116
        every { activity.components.settings } returns settings
        every { activity.settings() } returns settings
        every { activity.components.analytics } returns analytics
        every { analytics.metrics } returns metrics
117

118
119
        val restoreUseCase: TabsUseCases.RestoreUseCase = mockk(relaxed = true)

120
        controller = DefaultSessionControlController(
121
            activity = activity,
122
            store = store,
123
            settings = settings,
124
125
126
127
            engine = engine,
            metrics = metrics,
            tabCollectionStorage = tabCollectionStorage,
            addTabUseCase = tabsUseCases.addTab,
128
            reloadUrlUseCase = reloadUrlUseCase.reload,
129
            selectTabUseCase = selectTabUseCase.selectTab,
130
            restoreUseCase = restoreUseCase,
131
            fragmentStore = fragmentStore,
132
            navController = navController,
133
            viewLifecycleScope = scope,
134
135
            hideOnboarding = hideOnboarding,
            registerCollectionStorageObserver = registerCollectionStorageObserver,
136
            showDeleteCollectionPrompt = showDeleteCollectionPrompt,
137
138
            showTabTray = showTabTray,
            handleSwipedItemDeletionCancel = handleSwipedItemDeletionCancel
139
140
141
        )
    }

142
143
144
145
146
147
    @After
    fun cleanUp() {
        scope.cleanupTestCoroutines()
        testDispatcher.cleanupTestCoroutines()
    }

148
149
    @Test
    fun handleCollectionAddTabTapped() {
150
151
152
        val collection = mockk<TabCollection> {
            every { id } returns 12L
        }
153
        controller.handleCollectionAddTabTapped(collection)
154

155
        verify { metrics.track(Event.CollectionAddTabPressed) }
156
157
        verify {
            navController.navigate(
158
159
160
                match<NavDirections> {
                    it.actionId == R.id.action_global_collectionCreationFragment
                },
161
162
163
                null
            )
        }
164
165
166
    }

    @Test
167
168
169
170
171
    fun `handleCollectionOpenTabClicked onFailure`() {
        val tab = mockk<ComponentTab> {
            every { url } returns "https://mozilla.org"
            every { restore(activity, engine, restoreSessionId = false) } returns null
        }
172
        controller.handleCollectionOpenTabClicked(tab)
173

174
        verify { metrics.track(Event.CollectionTabRestored) }
175
176
177
178
179
180
181
182
183
184
185
        verify {
            activity.openToBrowserAndLoad(
                searchTermOrURL = "https://mozilla.org",
                newTab = true,
                from = BrowserDirection.FromHome
            )
        }
    }

    @Test
    fun `handleCollectionOpenTabClicked onTabRestored`() {
186
187
188
189
190
191
192
193
194
195
196
197
        val restoredTab = RecoverableTab(
            id = "test",
            parentId = null,
            url = "https://www.mozilla.org",
            title = "Mozilla",
            state = null,
            contextId = null,
            readerState = ReaderState(),
            lastAccess = 0,
            private = false
        )

198
        val tab = mockk<ComponentTab> {
199
            every { restore(activity, engine, restoreSessionId = false) } returns restoredTab
200
        }
201

202
203
204
205
        store.dispatch(TabListAction.AddTabAction(
            createTab(id = restoredTab.id, url = restoredTab.url))
        ).joinBlocking()
        store.dispatch(TabListAction.SelectTabAction(restoredTab.id)).joinBlocking()
206

207
208
209
        controller.handleCollectionOpenTabClicked(tab)
        verify { metrics.track(Event.CollectionTabRestored) }
        verify { activity.openToBrowser(BrowserDirection.FromHome) }
210
211
        verify { selectTabUseCase.selectTab.invoke(restoredTab.id) }
        verify { reloadUrlUseCase.reload.invoke(restoredTab.id) }
212
213
214
215
    }

    @Test
    fun handleCollectionOpenTabsTapped() {
216
217
218
        val collection = mockk<TabCollection> {
            every { tabs } returns emptyList()
        }
219
        controller.handleCollectionOpenTabsTapped(collection)
220

221
222
223
224
        verify { metrics.track(Event.CollectionAllTabsRestored) }
    }

    @Test
225
226
227
228
229
230
231
    fun `handleCollectionRemoveTab one tab`() {
        val collection = mockk<TabCollection> {
            every { tabs } returns listOf(mockk())
            every { title } returns "Collection"
        }
        val tab = mockk<ComponentTab>()
        every {
232
233
234
235
            activity.resources.getString(
                R.string.delete_tab_and_collection_dialog_title,
                "Collection"
            )
236
237
238
239
240
        } returns "Delete Collection?"
        every {
            activity.resources.getString(R.string.delete_tab_and_collection_dialog_message)
        } returns "Deleting this tab will delete everything."

241
        controller.handleCollectionRemoveTab(collection, tab, false)
242
243
244
245
246
247

        verify { metrics.track(Event.CollectionTabRemoved) }
        verify {
            showDeleteCollectionPrompt(
                collection,
                "Delete Collection?",
248
249
250
                "Deleting this tab will delete everything.",
                false,
                handleSwipedItemDeletionCancel
251
252
253
254
255
256
            )
        }
    }

    @Test
    fun `handleCollectionRemoveTab multiple tabs`() {
257
258
        val collection: TabCollection = mockk(relaxed = true)
        val tab: ComponentTab = mockk(relaxed = true)
259
        controller.handleCollectionRemoveTab(collection, tab, false)
260
261
262
263
264
        verify { metrics.track(Event.CollectionTabRemoved) }
    }

    @Test
    fun handleCollectionShareTabsClicked() {
265
266
        val collection = mockk<TabCollection> {
            every { tabs } returns emptyList()
267
            every { title } returns ""
268
        }
269
        controller.handleCollectionShareTabsClicked(collection)
270

271
        verify { metrics.track(Event.CollectionShared) }
272
273
274
275
276
277
        verify {
            navController.navigate(
                match<NavDirections> { it.actionId == R.id.action_global_shareFragment },
                null
            )
        }
278
279
280
281
    }

    @Test
    fun handleDeleteCollectionTapped() {
282
283
284
285
286
287
288
        val collection = mockk<TabCollection> {
            every { title } returns "Collection"
        }
        every {
            activity.resources.getString(R.string.tab_collection_dialog_message, "Collection")
        } returns "Are you sure you want to delete Collection?"

289
        controller.handleDeleteCollectionTapped(collection)
290
291
292
293
        verify {
            showDeleteCollectionPrompt(
                collection,
                null,
294
295
296
                "Are you sure you want to delete Collection?",
                false,
                handleSwipedItemDeletionCancel
297
298
            )
        }
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
    }

    @Test
    fun handlePrivateBrowsingLearnMoreClicked() {
        controller.handlePrivateBrowsingLearnMoreClicked()
        verify {
            activity.openToBrowserAndLoad(
                searchTermOrURL = SupportUtils.getGenericSumoURLForTopic
                    (SupportUtils.SumoTopic.PRIVATE_BROWSING_MYTHS),
                newTab = true,
                from = BrowserDirection.FromHome
            )
        }
    }

    @Test
    fun handleRenameCollectionTapped() {
316
317
318
        val collection = mockk<TabCollection> {
            every { id } returns 3L
        }
319
        controller.handleRenameCollectionTapped(collection)
320

321
        verify { metrics.track(Event.CollectionRenamePressed) }
322
323
324
325
326
327
        verify {
            navController.navigate(
                match<NavDirections> { it.actionId == R.id.action_global_collectionCreationFragment },
                null
            )
        }
328
329
    }

330
    @Test
331
    fun handleSelectDefaultTopSite() {
332
333
        val topSiteUrl = "mozilla.org"

334
        controller.handleSelectTopSite(topSiteUrl, TopSite.Type.DEFAULT)
335
336
        verify { metrics.track(Event.TopSiteOpenInNewTab) }
        verify { metrics.track(Event.TopSiteOpenDefault) }
337
338
339
340
341
342
343
        verify {
            tabsUseCases.addTab.invoke(
                topSiteUrl,
                selectTab = true,
                startLoading = true
            )
        }
344
345
346
347
348
349
350
        verify { activity.openToBrowser(BrowserDirection.FromHome) }
    }

    @Test
    fun handleSelectNonDefaultTopSite() {
        val topSiteUrl = "mozilla.org"

351
        controller.handleSelectTopSite(topSiteUrl, TopSite.Type.FRECENT)
352
        verify { metrics.track(Event.TopSiteOpenInNewTab) }
353
354
355
356
357
358
359
        verify {
            tabsUseCases.addTab.invoke(
                topSiteUrl,
                selectTab = true,
                startLoading = true
            )
        }
360
        verify { activity.openToBrowser(BrowserDirection.FromHome) }
361
362
    }

363
364
365
366
367
368
369
370
371
    @Test
    fun handleSelectGoogleDefaultTopSiteUS() {
        val topSiteUrl = SupportUtils.GOOGLE_URL

        store.dispatch(SearchAction.SetRegionAction(RegionState("US", "US"))).joinBlocking()

        controller.handleSelectTopSite(topSiteUrl, TopSite.Type.DEFAULT)
        verify { metrics.track(Event.TopSiteOpenInNewTab) }
        verify { metrics.track(Event.TopSiteOpenDefault) }
372
        verify { metrics.track(Event.TopSiteOpenGoogle) }
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
        verify {
            tabsUseCases.addTab.invoke(
                url = SupportUtils.GOOGLE_US_URL,
                selectTab = true,
                startLoading = true
            )
        }
        verify { activity.openToBrowser(BrowserDirection.FromHome) }
    }

    @Test
    fun handleSelectGoogleDefaultTopSiteXX() {
        val topSiteUrl = SupportUtils.GOOGLE_URL

        store.dispatch(SearchAction.SetRegionAction(RegionState("DE", "FR"))).joinBlocking()

        controller.handleSelectTopSite(topSiteUrl, TopSite.Type.DEFAULT)
        verify { metrics.track(Event.TopSiteOpenInNewTab) }
        verify { metrics.track(Event.TopSiteOpenDefault) }
392
        verify { metrics.track(Event.TopSiteOpenGoogle) }
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
        verify {
            tabsUseCases.addTab.invoke(
                SupportUtils.GOOGLE_XX_URL,
                selectTab = true,
                startLoading = true
            )
        }
        verify { activity.openToBrowser(BrowserDirection.FromHome) }
    }

    @Test
    fun handleSelectGooglePinnedTopSiteUS() {
        val topSiteUrl = SupportUtils.GOOGLE_URL

        store.dispatch(SearchAction.SetRegionAction(RegionState("US", "US"))).joinBlocking()

        controller.handleSelectTopSite(topSiteUrl, TopSite.Type.PINNED)
        verify { metrics.track(Event.TopSiteOpenInNewTab) }
        verify { metrics.track(Event.TopSiteOpenPinned) }
412
        verify { metrics.track(Event.TopSiteOpenGoogle) }
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
        verify {
            tabsUseCases.addTab.invoke(
                SupportUtils.GOOGLE_US_URL,
                selectTab = true,
                startLoading = true
            )
        }
        verify { activity.openToBrowser(BrowserDirection.FromHome) }
    }

    @Test
    fun handleSelectGooglePinnedTopSiteXX() {
        val topSiteUrl = SupportUtils.GOOGLE_URL

        store.dispatch(SearchAction.SetRegionAction(RegionState("DE", "FR"))).joinBlocking()

        controller.handleSelectTopSite(topSiteUrl, TopSite.Type.PINNED)
        verify { metrics.track(Event.TopSiteOpenInNewTab) }
        verify { metrics.track(Event.TopSiteOpenPinned) }
432
        verify { metrics.track(Event.TopSiteOpenGoogle) }
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
        verify {
            tabsUseCases.addTab.invoke(
                SupportUtils.GOOGLE_XX_URL,
                selectTab = true,
                startLoading = true
            )
        }
        verify { activity.openToBrowser(BrowserDirection.FromHome) }
    }

    @Test
    fun handleSelectGoogleFrecentTopSiteUS() {
        val topSiteUrl = SupportUtils.GOOGLE_URL

        store.dispatch(SearchAction.SetRegionAction(RegionState("US", "US"))).joinBlocking()

        controller.handleSelectTopSite(topSiteUrl, TopSite.Type.FRECENT)
        verify { metrics.track(Event.TopSiteOpenInNewTab) }
        verify { metrics.track(Event.TopSiteOpenFrecent) }
452
        verify { metrics.track(Event.TopSiteOpenGoogle) }
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
        verify {
            tabsUseCases.addTab.invoke(
                SupportUtils.GOOGLE_US_URL,
                selectTab = true,
                startLoading = true
            )
        }
        verify { activity.openToBrowser(BrowserDirection.FromHome) }
    }

    @Test
    fun handleSelectGoogleFrecentTopSiteXX() {
        val topSiteUrl = SupportUtils.GOOGLE_URL

        store.dispatch(SearchAction.SetRegionAction(RegionState("DE", "FR"))).joinBlocking()

        controller.handleSelectTopSite(topSiteUrl, TopSite.Type.FRECENT)
        verify { metrics.track(Event.TopSiteOpenInNewTab) }
        verify { metrics.track(Event.TopSiteOpenFrecent) }
472
        verify { metrics.track(Event.TopSiteOpenGoogle) }
473
474
475
476
477
478
479
480
481
482
        verify {
            tabsUseCases.addTab.invoke(
                SupportUtils.GOOGLE_XX_URL,
                selectTab = true,
                startLoading = true
            )
        }
        verify { activity.openToBrowser(BrowserDirection.FromHome) }
    }

483
484
485
486
487
488
    @Test
    fun handleStartBrowsingClicked() {
        controller.handleStartBrowsingClicked()
        verify { hideOnboarding() }
    }

489
490
491
    @Test
    fun handleOpenSettingsClicked() {
        controller.handleOpenSettingsClicked()
492
493
494
495
496
497
498
499
500
501
502
        verify {
            navController.navigate(
                match<NavDirections> { it.actionId == R.id.action_global_privateBrowsingFragment },
                null
            )
        }
    }

    @Test
    fun handleWhatsNewGetAnswersClicked() {
        controller.handleWhatsNewGetAnswersClicked()
503
        val whatsNewUrl = SupportUtils.getWhatsNewUrl(activity)
504
505
        verify {
            activity.openToBrowserAndLoad(
506
                searchTermOrURL = whatsNewUrl,
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
                newTab = true,
                from = BrowserDirection.FromHome
            )
        }
    }

    @Test
    fun handleReadPrivacyNoticeClicked() {
        controller.handleReadPrivacyNoticeClicked()
        verify {
            activity.openToBrowserAndLoad(
                searchTermOrURL = SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.PRIVATE_NOTICE),
                newTab = true,
                from = BrowserDirection.FromHome
            )
        }
523
524
    }

525
526
    @Test
    fun handleToggleCollectionExpanded() {
527
        val collection = mockk<TabCollection>()
528
        controller.handleToggleCollectionExpanded(collection, true)
529
        verify { fragmentStore.dispatch(HomeFragmentAction.CollectionExpanded(collection, true)) }
530
    }
531

532
533
534
535
536
537
538
    @Test
    fun handleCloseTip() {
        val tip = mockk<Tip>()
        controller.handleCloseTip(tip)
        verify { fragmentStore.dispatch(HomeFragmentAction.RemoveTip(tip)) }
    }

539
540
541
    @Test
    fun handleCreateCollection() {
        controller.handleCreateCollection()
542
543
544

        verify {
            navController.navigate(
545
                match<NavDirections> { it.actionId == R.id.action_global_tabTrayDialogFragment },
546
547
548
                null
            )
        }
549
    }
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582

    @Test
    fun handlePasteAndGo() {
        controller.handlePasteAndGo("text")

        verify {
            activity.openToBrowserAndLoad(
                searchTermOrURL = "text",
                newTab = true,
                from = BrowserDirection.FromHome,
                engine = searchEngine
            )
            metrics.track(any<Event.PerformedSearch>())
        }

        controller.handlePasteAndGo("https://mozilla.org")
        verify {
            activity.openToBrowserAndLoad(
                searchTermOrURL = "https://mozilla.org",
                newTab = true,
                from = BrowserDirection.FromHome,
                engine = searchEngine
            )
            metrics.track(any<Event.EnteredUrl>())
        }
    }

    @Test
    fun handlePaste() {
        controller.handlePaste("text")

        verify {
            navController.navigate(
583
                match<NavDirections> { it.actionId == R.id.action_global_search_dialog },
584
585
586
587
                null
            )
        }
    }
588
589
590
591
592
593
594
595
596
597

    @Test
    fun handleRemoveCollectionsPlaceholder() {
        controller.handleRemoveCollectionsPlaceholder()

        verify {
            settings.showCollectionsPlaceholderOnHome = false
            fragmentStore.dispatch(HomeFragmentAction.RemoveCollectionsPlaceholder)
        }
    }
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623

    @Test
    fun handleMenuOpenedWhileSearchShowing() {
        every { navController.currentDestination } returns mockk {
            every { id } returns R.id.searchDialogFragment
        }

        controller.handleMenuOpened()

        verify {
            navController.navigateUp()
        }
    }

    @Test
    fun handleMenuOpenedWhileSearchNotShowing() {
        every { navController.currentDestination } returns mockk {
            every { id } returns R.id.homeFragment
        }

        controller.handleMenuOpened()

        verify(exactly = 0) {
            navController.navigateUp()
        }
    }
624
}