From 6c8b1a7e8f47051fbd735c0a7ee55b382a3e47dd Mon Sep 17 00:00:00 2001 From: Jonathan Almeida Date: Thu, 15 Apr 2021 00:42:54 -0400 Subject: [PATCH] Close #19045: Dismiss tabstray when last tab in a page is closed --- .../fenix/tabstray/CloseOnLastTabBinding.kt | 50 ++++++++ .../fenix/tabstray/TabsTrayFragment.kt | 11 ++ .../tabstray/CloseOnLastTabBindingTest.kt | 115 ++++++++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 app/src/main/java/org/mozilla/fenix/tabstray/CloseOnLastTabBinding.kt create mode 100644 app/src/test/java/org/mozilla/fenix/tabstray/CloseOnLastTabBindingTest.kt diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/CloseOnLastTabBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/CloseOnLastTabBinding.kt new file mode 100644 index 000000000..d19958375 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/CloseOnLastTabBinding.kt @@ -0,0 +1,50 @@ +/* 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.tabstray + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.map +import mozilla.components.browser.state.selector.normalTabs +import mozilla.components.browser.state.selector.privateTabs +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged +import org.mozilla.fenix.components.AbstractBinding + +/** + * A binding that closes the tabs tray when the last tab is closed. + */ +class CloseOnLastTabBinding( + browserStore: BrowserStore, + private val tabsTrayStore: TabsTrayStore, + private val navigationInteractor: NavigationInteractor +) : AbstractBinding(browserStore) { + override suspend fun onState(flow: Flow) { + flow.map { it } + // Ignore the initial state; we don't want to close immediately. + .drop(1) + .ifChanged { it.tabs } + .collect { state -> + val selectedPage = tabsTrayStore.state.selectedPage + val tabs = when (selectedPage) { + Page.NormalTabs -> { + state.normalTabs + } + Page.PrivateTabs -> { + state.privateTabs + } + else -> { + // Do nothing if we're on any other non-browser page. + null + } + } + if (tabs?.isEmpty() == true) { + navigationInteractor.onCloseAllTabsClicked(selectedPage == Page.PrivateTabs) + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt index 361e47786..1d606fa3b 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt @@ -59,6 +59,7 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor { private val selectionBannerBinding = ViewBoundFeatureWrapper() private val selectionHandleBinding = ViewBoundFeatureWrapper() private val tabsTrayCtaBinding = ViewBoundFeatureWrapper() + private val closeOnLastTabBinding = ViewBoundFeatureWrapper() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -228,6 +229,16 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor { owner = this, view = view ) + + closeOnLastTabBinding.set( + feature = CloseOnLastTabBinding( + browserStore = requireComponents.core.store, + tabsTrayStore = tabsTrayStore, + navigationInteractor = navigationInteractor + ), + owner = this, + view = view + ) } override fun setCurrentTrayPosition(position: Int, smoothScroll: Boolean) { diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/CloseOnLastTabBindingTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/CloseOnLastTabBindingTest.kt new file mode 100644 index 000000000..99d17ac81 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/tabstray/CloseOnLastTabBindingTest.kt @@ -0,0 +1,115 @@ +/* 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.tabstray + +import io.mockk.Called +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineDispatcher +import mozilla.components.browser.state.action.TabListAction +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.createTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.support.test.libstate.ext.waitUntilIdle +import mozilla.components.support.test.rule.MainCoroutineRule +import org.junit.Rule +import org.junit.Test + +class CloseOnLastTabBindingTest { + + @OptIn(ExperimentalCoroutinesApi::class) + @get:Rule + val coroutinesTestRule = MainCoroutineRule(TestCoroutineDispatcher()) + + @Test + fun `WHEN the binding starts THEN do nothing`() { + val browserStore = BrowserStore() + val tabsTrayStore = TabsTrayStore() + val interactor = mockk(relaxed = true) + val binding = CloseOnLastTabBinding(browserStore, tabsTrayStore, interactor) + + binding.start() + + verify { interactor wasNot Called } + } + + @Test + fun `WHEN a tab is closed THEN invoke the interactor`() { + val browserStore = BrowserStore( + BrowserState( + tabs = listOf( + createTab( + "https://mozilla.org", + id = "tab1" + ) + ) + ) + ) + val tabsTrayStore = TabsTrayStore() + val interactor = mockk(relaxed = true) + val binding = CloseOnLastTabBinding(browserStore, tabsTrayStore, interactor) + + binding.start() + + browserStore.dispatch(TabListAction.RemoveTabAction("tab1")) + + browserStore.waitUntilIdle() + + verify { interactor.onCloseAllTabsClicked(false) } + } + + @Test + fun `WHEN a private tab is closed THEN invoke the interactor`() { + val browserStore = BrowserStore( + BrowserState( + tabs = listOf( + createTab( + "https://mozilla.org", + id = "tab1", + private = true + ) + ) + ) + ) + val tabsTrayStore = TabsTrayStore(TabsTrayState(selectedPage = Page.PrivateTabs)) + val interactor = mockk(relaxed = true) + val binding = CloseOnLastTabBinding(browserStore, tabsTrayStore, interactor) + + binding.start() + + browserStore.dispatch(TabListAction.RemoveTabAction("tab1")) + + browserStore.waitUntilIdle() + + verify { interactor.onCloseAllTabsClicked(true) } + } + + @Test + fun `WHEN on the synced tabs page THEN nothing is invoked`() { + val browserStore = BrowserStore( + BrowserState( + tabs = listOf( + createTab( + "https://mozilla.org", + id = "tab1", + private = true + ) + ) + ) + ) + val tabsTrayStore = TabsTrayStore(TabsTrayState(selectedPage = Page.SyncedTabs)) + val interactor = mockk(relaxed = true) + val binding = CloseOnLastTabBinding(browserStore, tabsTrayStore, interactor) + + binding.start() + + browserStore.dispatch(TabListAction.RemoveAllTabsAction) + + browserStore.waitUntilIdle() + + verify { interactor wasNot Called } + } +}