[fenix] Close https://github.com/mozilla-mobile/fenix/issues/22305: Use TabsTrayStore for populating adapters

pull/600/head
Jonathan Almeida 3 years ago committed by mergify[bot]
parent 35556e4cc2
commit 199547f94f

@ -26,6 +26,7 @@ import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.tabs.tabstray.TabsFeature
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.NavGraphDirections
@ -48,6 +49,7 @@ import org.mozilla.fenix.tabstray.browser.DefaultBrowserTrayInteractor
import org.mozilla.fenix.tabstray.browser.SelectionBannerBinding
import org.mozilla.fenix.tabstray.browser.SelectionBannerBinding.VisibilityModifier
import org.mozilla.fenix.tabstray.browser.SelectionHandleBinding
import org.mozilla.fenix.tabstray.browser.TabSorter
import org.mozilla.fenix.tabstray.ext.anchorWithAction
import org.mozilla.fenix.tabstray.ext.bookmarkMessage
import org.mozilla.fenix.tabstray.ext.collectionMessage
@ -72,6 +74,7 @@ class TabsTrayFragment : AppCompatDialogFragment() {
private val selectionHandleBinding = ViewBoundFeatureWrapper<SelectionHandleBinding>()
private val tabsTrayCtaBinding = ViewBoundFeatureWrapper<TabsTrayInfoBannerBinding>()
private val secureTabsTrayBinding = ViewBoundFeatureWrapper<SecureTabsTrayBinding>()
private val tabsFeature = ViewBoundFeatureWrapper<TabsFeature>()
private val tabsTrayInactiveTabsOnboardingBinding = ViewBoundFeatureWrapper<TabsTrayInactiveTabsOnboardingBinding>()
@VisibleForTesting @Suppress("VariableNaming")
@ -223,6 +226,19 @@ class TabsTrayFragment : AppCompatDialogFragment() {
displayMetrics = requireContext().resources.displayMetrics
)
tabsFeature.set(
feature = TabsFeature(
tabsTray = TabSorter(
requireContext().settings(),
requireContext().components.analytics.metrics,
tabsTrayStore
),
store = requireContext().components.core.store,
),
owner = this,
view = view
)
tabsTrayCtaBinding.set(
feature = TabsTrayInfoBannerBinding(
context = view.context,

@ -7,8 +7,6 @@ package org.mozilla.fenix.tabstray.browser
import android.content.Context
import android.util.AttributeSet
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.feature.tabs.tabstray.TabsFeature
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.tabstray.TabsTrayInteractor
import org.mozilla.fenix.tabstray.TabsTrayStore
@ -24,27 +22,6 @@ abstract class AbstractBrowserTrayList @JvmOverloads constructor(
lateinit var interactor: TabsTrayInteractor
lateinit var tabsTrayStore: TabsTrayStore
/**
* A [TabsFeature] is required for each browser list to ensure one always exists for displaying
* tabs.
*/
abstract val tabsFeature: TabsFeature
// NB: The use cases here are duplicated because there isn't a nicer
// way to share them without a better dependency injection solution.
protected val selectTabUseCase = SelectTabUseCaseWrapper(
context.components.analytics.metrics,
context.components.useCases.tabsUseCases.selectTab
) {
interactor.onBrowserTabSelected()
}
protected val removeTabUseCase = RemoveTabUseCaseWrapper(
context.components.analytics.metrics
) { sessionId ->
interactor.onDeleteTab(sessionId)
}
protected val swipeToDelete by lazy {
SwipeToDeleteBinding(tabsTrayStore)
}

@ -41,7 +41,7 @@ class InactiveTabsAdapter(
) : Adapter(DiffCallback), TabsTray, FeatureNameHolder {
internal lateinit var inactiveTabsInteractor: InactiveTabsInteractor
internal var inActiveTabsCount: Int = 0
private var inActiveTabsCount: Int = 0
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InactiveTabViewHolder {
val view = LayoutInflater.from(parent.context)

@ -0,0 +1,31 @@
/* 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.browser
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import mozilla.components.browser.tabstray.TabsTray
import mozilla.components.lib.state.helpers.AbstractBinding
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.tabstray.TabsTrayState
import org.mozilla.fenix.tabstray.TabsTrayStore
/**
* An inactive tabs observer that updates the provided [TabsTray].
*/
class InactiveTabsBinding(
store: TabsTrayStore,
private val tray: TabsTray
) : AbstractBinding<TabsTrayState>(store) {
override suspend fun onState(flow: Flow<TabsTrayState>) {
flow.map { it.inactiveTabs }
.ifChanged()
.collect {
// We pass null for the selected tab id here, because inactive tabs doesn't care.
tray.updateTabs(it, null)
}
}
}

@ -5,19 +5,17 @@
package org.mozilla.fenix.tabstray.browser
import androidx.annotation.VisibleForTesting
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.tabstray.TabsTray
import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction.UpdateInactiveExpanded
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.tabstray.TabsTrayStore
import org.mozilla.fenix.utils.Settings
class InactiveTabsController(
private val browserStore: BrowserStore,
private val tabsTrayStore: TabsTrayStore,
private val appStore: AppStore,
private val tabFilter: (TabSessionState) -> Boolean,
private val tray: TabsTray,
private val metrics: MetricController,
private val settings: Settings
@ -27,7 +25,11 @@ class InactiveTabsController(
* the title showing.
*/
fun updateCardExpansion(isExpanded: Boolean) {
appStore.dispatch(AppAction.UpdateInactiveExpanded(isExpanded))
appStore.dispatch(UpdateInactiveExpanded(isExpanded)).invokeOnCompletion {
// To avoid racing, we read the list of inactive tabs only after we have updated
// the expanded state.
refreshInactiveTabsSection()
}
metrics.track(
when (isExpanded) {
@ -35,8 +37,6 @@ class InactiveTabsController(
false -> Event.TabsTrayInactiveTabsCollapsed
}
)
refreshInactiveTabsSection()
}
/**
@ -70,7 +70,7 @@ class InactiveTabsController(
@VisibleForTesting
internal fun refreshInactiveTabsSection() {
val tabs = browserStore.state.tabs.filter(tabFilter)
tray.updateTabs(tabs, browserStore.state.selectedTabId)
val tabs = tabsTrayStore.state.inactiveTabs
tray.updateTabs(tabs, null)
}
}

@ -7,15 +7,13 @@ package org.mozilla.fenix.tabstray.browser
import android.content.Context
import android.util.AttributeSet
import androidx.recyclerview.widget.ConcatAdapter
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.tabstray.TabViewHolder
import mozilla.components.feature.tabs.tabstray.TabsFeature
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.maxActiveTime
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.tabstray.ext.browserAdapter
import org.mozilla.fenix.tabstray.ext.inactiveTabsAdapter
import org.mozilla.fenix.tabstray.ext.isNormalTabInactive
import org.mozilla.fenix.tabstray.ext.tabGroupAdapter
import org.mozilla.fenix.tabstray.ext.titleHeaderAdapter
class NormalBrowserTrayList @JvmOverloads constructor(
context: Context,
@ -24,26 +22,28 @@ class NormalBrowserTrayList @JvmOverloads constructor(
) : AbstractBrowserTrayList(context, attrs, defStyleAttr) {
private val concatAdapter by lazy { adapter as ConcatAdapter }
private val tabSorter by lazy {
TabSorter(
context.settings(),
context.components.analytics.metrics,
concatAdapter
)
private val inactiveTabsBinding by lazy {
InactiveTabsBinding(tabsTrayStore, concatAdapter.inactiveTabsAdapter)
}
private val inactiveTabsFilter: (TabSessionState) -> Boolean = filter@{
if (!context.settings().inactiveTabsAreEnabled) {
return@filter false
}
it.isNormalTabInactive(maxActiveTime)
private val normalTabsBinding by lazy {
NormalTabsBinding(tabsTrayStore, context.components.core.store, concatAdapter.browserAdapter)
}
private val titleHeaderBinding by lazy {
OtherHeaderBinding(tabsTrayStore) { concatAdapter.titleHeaderAdapter.handleListChanges(it) }
}
private val tabGroupBinding by lazy {
TabGroupBinding(tabsTrayStore) { concatAdapter.tabGroupAdapter.submitList(it) }
}
private val inactiveTabsInteractor by lazy {
DefaultInactiveTabsInteractor(
InactiveTabsController(
context.components.core.store,
tabsTrayStore,
context.components.appStore,
inactiveTabsFilter,
concatAdapter.inactiveTabsAdapter,
context.components.analytics.metrics,
context.settings()
@ -51,13 +51,6 @@ class NormalBrowserTrayList @JvmOverloads constructor(
)
}
override val tabsFeature by lazy {
TabsFeature(
tabSorter,
context.components.core.store,
) { !it.content.private }
}
private val touchHelper by lazy {
TabsTouchHelper(
interactionDelegate = concatAdapter.browserAdapter.interactor,
@ -74,7 +67,10 @@ class NormalBrowserTrayList @JvmOverloads constructor(
concatAdapter.inactiveTabsAdapter.inactiveTabsInteractor = inactiveTabsInteractor
tabsFeature.start()
inactiveTabsBinding.start()
normalTabsBinding.start()
titleHeaderBinding.start()
tabGroupBinding.start()
touchHelper.attachToRecyclerView(this)
}
@ -82,7 +78,10 @@ class NormalBrowserTrayList @JvmOverloads constructor(
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
tabsFeature.stop()
inactiveTabsBinding.stop()
normalTabsBinding.stop()
titleHeaderBinding.stop()
tabGroupBinding.stop()
touchHelper.attachToRecyclerView(null)
}

@ -0,0 +1,33 @@
/* 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.browser
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.tabstray.TabsTray
import mozilla.components.lib.state.helpers.AbstractBinding
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.tabstray.TabsTrayState
import org.mozilla.fenix.tabstray.TabsTrayStore
/**
* A normal tabs observer that updates the provided [TabsTray].
*/
class NormalTabsBinding(
store: TabsTrayStore,
private val browserStore: BrowserStore,
private val tabsTray: TabsTray
) : AbstractBinding<TabsTrayState>(store) {
override suspend fun onState(flow: Flow<TabsTrayState>) {
flow.map { it.normalTabs }
.ifChanged()
.collect {
// Getting the selectedTabId from the BrowserStore at a different time might lead to a race.
tabsTray.updateTabs(it, browserStore.state.selectedTabId)
}
}
}

@ -0,0 +1,31 @@
/* 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.browser
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import mozilla.components.lib.state.helpers.AbstractBinding
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import org.mozilla.fenix.tabstray.TabsTrayState
import org.mozilla.fenix.tabstray.TabsTrayStore
/**
* A tabs observer that informs [showHeader] if an "Other tabs" title should be displayed in the tray.
*/
class OtherHeaderBinding(
store: TabsTrayStore,
private val showHeader: (Boolean) -> Unit
) : AbstractBinding<TabsTrayState>(store) {
override suspend fun onState(flow: Flow<TabsTrayState>) {
flow.ifAnyChanged { arrayOf(it.normalTabs, it.searchTermGroups) }
.collect {
if (it.searchTermGroups.isNotEmpty() && it.normalTabs.isNotEmpty()) {
showHeader(true)
} else {
showHeader(false)
}
}
}
}

@ -8,7 +8,6 @@ import android.content.Context
import android.util.AttributeSet
import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE
import mozilla.components.feature.tabs.tabstray.TabsFeature
import org.mozilla.fenix.ext.components
class PrivateBrowserTrayList @JvmOverloads constructor(
@ -17,14 +16,10 @@ class PrivateBrowserTrayList @JvmOverloads constructor(
defStyleAttr: Int = 0
) : AbstractBrowserTrayList(context, attrs, defStyleAttr) {
override val tabsFeature by lazy {
// NB: The use cases here are duplicated because there isn't a nicer
// way to share them without a better dependency injection solution.
TabsFeature(
adapter as BrowserTabsAdapter,
context.components.core.store,
) { it.content.private }
private val privateTabsBinding by lazy {
PrivateTabsBinding(tabsTrayStore, context.components.core.store, adapter as BrowserTabsAdapter)
}
private val touchHelper by lazy {
TabsTouchHelper(
interactionDelegate = (adapter as BrowserTabsAdapter).delegate,
@ -37,7 +32,7 @@ class PrivateBrowserTrayList @JvmOverloads constructor(
override fun onAttachedToWindow() {
super.onAttachedToWindow()
tabsFeature.start()
privateTabsBinding.start()
swipeToDelete.start()
adapter?.onAttachedToRecyclerView(this)
@ -49,7 +44,7 @@ class PrivateBrowserTrayList @JvmOverloads constructor(
public override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
tabsFeature.stop()
privateTabsBinding.stop()
swipeToDelete.stop()
// Notify the adapter that it is released from the view preemptively.

@ -0,0 +1,33 @@
/* 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.browser
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.tabstray.TabsTray
import mozilla.components.lib.state.helpers.AbstractBinding
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.tabstray.TabsTrayState
import org.mozilla.fenix.tabstray.TabsTrayStore
/**
* A private tabs observer that updates the provided [TabsTray].
*/
class PrivateTabsBinding(
store: TabsTrayStore,
private val browserStore: BrowserStore,
private val tray: TabsTray
) : AbstractBinding<TabsTrayState>(store) {
override suspend fun onState(flow: Flow<TabsTrayState>) {
flow.map { it.privateTabs }
.ifChanged()
.collect {
// Getting the selectedTabId from the BrowserStore at a different time might lead to a race.
tray.updateTabs(it, browserStore.state.selectedTabId)
}
}
}

@ -0,0 +1,29 @@
/* 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.browser
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import mozilla.components.lib.state.helpers.AbstractBinding
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.tabstray.TabsTrayState
import org.mozilla.fenix.tabstray.TabsTrayStore
/**
* A search-term tab group observer that updates the provided [tray].
*/
class TabGroupBinding(
store: TabsTrayStore,
private val tray: (List<TabGroup>) -> Unit
) : AbstractBinding<TabsTrayState>(store) {
override suspend fun onState(flow: Flow<TabsTrayState>) {
flow.map { it.searchTermGroups }
.ifChanged()
.collect {
tray.invoke(it)
}
}
}

@ -4,7 +4,6 @@
package org.mozilla.fenix.tabstray.browser
import androidx.recyclerview.widget.ConcatAdapter
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.tabstray.TabsTray
import mozilla.components.feature.tabs.tabstray.TabsFeature
@ -14,13 +13,9 @@ import org.mozilla.fenix.ext.maxActiveTime
import org.mozilla.fenix.ext.toSearchGroup
import org.mozilla.fenix.tabstray.TabsTrayAction
import org.mozilla.fenix.tabstray.TabsTrayStore
import org.mozilla.fenix.tabstray.ext.browserAdapter
import org.mozilla.fenix.tabstray.ext.hasSearchTerm
import org.mozilla.fenix.tabstray.ext.inactiveTabsAdapter
import org.mozilla.fenix.tabstray.ext.isActive
import org.mozilla.fenix.tabstray.ext.isNormalTabActiveWithSearchTerm
import org.mozilla.fenix.tabstray.ext.tabGroupAdapter
import org.mozilla.fenix.tabstray.ext.titleHeaderAdapter
import org.mozilla.fenix.utils.Settings
/**
@ -29,7 +24,6 @@ import org.mozilla.fenix.utils.Settings
class TabSorter(
private val settings: Settings,
private val metrics: MetricController,
private val concatAdapter: ConcatAdapter? = null,
private val tabsTrayStore: TabsTrayStore? = null
) : TabsTray {
private var shouldReportMetrics: Boolean = true
@ -37,35 +31,29 @@ class TabSorter(
override fun updateTabs(tabs: List<TabSessionState>, selectedTabId: String?) {
val privateTabs = tabs.filter { it.content.private }
tabsTrayStore?.dispatch(TabsTrayAction.UpdatePrivateTabs(privateTabs))
val allNormalTabs = tabs - privateTabs
val inactiveTabs = allNormalTabs.getInactiveTabs(settings)
val searchTermTabs = allNormalTabs.getSearchGroupTabs(settings)
val normalTabs = allNormalTabs - inactiveTabs - searchTermTabs
val normalTabs = tabs - privateTabs
val inactiveTabs = normalTabs.getInactiveTabs(settings)
val searchTermTabs = normalTabs.getSearchGroupTabs(settings)
val regularTabs = normalTabs - inactiveTabs - searchTermTabs
// Private tabs
tabsTrayStore?.dispatch(TabsTrayAction.UpdatePrivateTabs(privateTabs))
// Inactive tabs
tabsTrayStore?.dispatch(TabsTrayAction.UpdateInactiveTabs(inactiveTabs))
// Tab groups
// We don't need to provide a selectedId, because the [TabGroupAdapter] has that built-in with support from
// NormalBrowserPageViewHolder.scrollToTab.
val (groups, remainderTabs) = searchTermTabs.toSearchGroup(groupsSet)
groupsSet.clear()
groupsSet.addAll(groups.map { it.searchTerm })
concatAdapter?.tabGroupAdapter?.submitList(groups)
tabsTrayStore?.dispatch(TabsTrayAction.UpdateSearchGroupTabs(groups))
// Normal tabs.
val totalNormalTabs = (regularTabs + remainderTabs)
concatAdapter?.browserAdapter?.updateTabs(totalNormalTabs, selectedTabId)
val totalNormalTabs = (normalTabs + remainderTabs)
tabsTrayStore?.dispatch(TabsTrayAction.UpdateNormalTabs(totalNormalTabs))
// Normal tab title header.
concatAdapter?.titleHeaderAdapter
?.handleListChanges(totalNormalTabs.isNotEmpty() && groups.isNotEmpty())
// TODO move this to a middleware in the TabsTrayStore.
if (shouldReportMetrics) {
shouldReportMetrics = false

@ -0,0 +1,58 @@
/* 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.browser
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.tabstray.TabsTray
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.After
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.tabstray.TabsTrayAction
import org.mozilla.fenix.tabstray.TabsTrayStore
class InactiveTabsBindingTest {
val store = TabsTrayStore()
val tray: TabsTray = mockk(relaxed = true)
val binding = InactiveTabsBinding(store, tray)
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
@After
fun teardown() {
binding.stop()
}
@Test
fun `WHEN the store is updated THEN notify the tabs tray`() {
assertTrue(store.state.inactiveTabs.isEmpty())
store.dispatch(TabsTrayAction.UpdateInactiveTabs(listOf(createTab("https://mozilla.org")))).joinBlocking()
binding.start()
assertTrue(store.state.inactiveTabs.isNotEmpty())
verify { tray.updateTabs(any(), any()) }
}
@Test
fun `WHEN non-inactive tabs are updated THEN do not notify the tabs tray`() {
assertTrue(store.state.inactiveTabs.isEmpty())
store.dispatch(TabsTrayAction.UpdatePrivateTabs(listOf(createTab("https://mozilla.org")))).joinBlocking()
binding.start()
assertTrue(store.state.inactiveTabs.isEmpty())
verify { tray.updateTabs(emptyList(), null) }
}
}

@ -11,7 +11,6 @@ import io.mockk.mockk
import io.mockk.slot
import io.mockk.spyk
import io.mockk.verify
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.tabstray.TabsTray
@ -21,6 +20,8 @@ import org.junit.Test
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.tabstray.TabsTrayState
import org.mozilla.fenix.tabstray.TabsTrayStore
import org.mozilla.fenix.utils.Settings
class InactiveTabsControllerTest {
@ -31,20 +32,18 @@ class InactiveTabsControllerTest {
@Test
fun `WHEN expanded THEN notify filtered card`() {
val filter: (TabSessionState) -> Boolean = { !it.content.private }
val store = BrowserStore(
BrowserState(
tabs = listOf(
val store = TabsTrayStore(
TabsTrayState(
inactiveTabs = listOf(
createTabState("https://mozilla.org", id = "1"),
createTabState("https://firefox.com", id = "2"),
createTabState("https://getpocket.com", id = "3", private = true)
createTabState("https://firefox.com", id = "2")
)
)
)
val tray: TabsTray = mockk(relaxed = true)
val tabsSlot = slot<List<TabSessionState>>()
val controller =
InactiveTabsController(store, appStore, filter, tray, mockk(relaxed = true), settings)
InactiveTabsController(store, appStore, tray, mockk(relaxed = true), settings)
controller.updateCardExpansion(true)
@ -56,9 +55,9 @@ class InactiveTabsControllerTest {
@Test
fun `WHEN expanded THEN track telemetry event`() {
val store = BrowserStore(BrowserState())
val store = TabsTrayStore()
val controller = InactiveTabsController(
store, appStore, mockk(relaxed = true), mockk(relaxed = true), metrics, settings
store, appStore, mockk(relaxed = true), metrics, settings
)
controller.updateCardExpansion(true)
@ -68,9 +67,9 @@ class InactiveTabsControllerTest {
@Test
fun `WHEN collapsed THEN track telemetry event`() {
val store = BrowserStore(BrowserState())
val store = TabsTrayStore()
val controller = InactiveTabsController(
store, appStore, mockk(relaxed = true), mockk(relaxed = true), metrics, settings
store, appStore, mockk(relaxed = true), metrics, settings
)
controller.updateCardExpansion(false)
@ -80,10 +79,10 @@ class InactiveTabsControllerTest {
@Test
fun `WHEN close THEN update settings and refresh`() {
val store = BrowserStore()
val store = TabsTrayStore()
val controller = spyk(
InactiveTabsController(
store, appStore, mockk(relaxed = true), mockk(relaxed = true), metrics, settings
store, appStore, mockk(relaxed = true), metrics, settings
)
)

@ -0,0 +1,68 @@
/* 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.browser
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.tabstray.TabsTray
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.tabstray.TabsTrayAction
import org.mozilla.fenix.tabstray.TabsTrayStore
class NormalTabsBindingTest {
val store = TabsTrayStore()
val browserStore = BrowserStore(BrowserState(tabs = listOf(createTab("", id = "1")), selectedTabId = "1"))
val tray: TabsTray = mockk(relaxed = true)
val binding = NormalTabsBinding(store, browserStore, tray)
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
@After
fun teardown() {
binding.stop()
}
@Test
fun `WHEN the store is updated THEN notify the tabs tray`() {
val slotTabs = slot<List<TabSessionState>>()
val expectedTabs = listOf(createTab("https://mozilla.org"))
assertTrue(store.state.normalTabs.isEmpty())
store.dispatch(TabsTrayAction.UpdateNormalTabs(expectedTabs)).joinBlocking()
binding.start()
assertTrue(store.state.normalTabs.isNotEmpty())
verify { tray.updateTabs(capture(slotTabs), "1") }
assertEquals(expectedTabs, slotTabs.captured)
}
@Test
fun `WHEN non-inactive tabs are updated THEN do not notify the tabs tray`() {
assertTrue(store.state.normalTabs.isEmpty())
store.dispatch(TabsTrayAction.UpdatePrivateTabs(listOf(createTab("https://mozilla.org")))).joinBlocking()
binding.start()
assertTrue(store.state.normalTabs.isEmpty())
verify { tray.updateTabs(emptyList(), "1") }
}
}

@ -0,0 +1,73 @@
/* 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.browser
import io.mockk.mockk
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.tabstray.TabsTrayState
import org.mozilla.fenix.tabstray.TabsTrayStore
class OtherHeaderBindingTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
@Test
fun `WHEN there are no tabs THEN show no header`() {
val store = TabsTrayStore()
var result: Boolean? = null
val binding = OtherHeaderBinding(store) { result = it }
binding.start()
store.waitUntilIdle()
assertFalse(result!!)
}
@Test
fun `WHEN tabs for only groups THEN show no header`() {
val store = TabsTrayStore(TabsTrayState(searchTermGroups = listOf(mockk())))
var result: Boolean? = null
val binding = OtherHeaderBinding(store) { result = it }
binding.start()
store.waitUntilIdle()
assertFalse(result!!)
}
@Test
fun `WHEN tabs for only normal tabs THEN show no header`() {
val store = TabsTrayStore(TabsTrayState(normalTabs = listOf(mockk())))
var result: Boolean? = null
val binding = OtherHeaderBinding(store) { result = it }
binding.start()
store.waitUntilIdle()
assertFalse(result!!)
}
@Test
fun `WHEN normal tabs and groups exist THEN show header`() {
val store = TabsTrayStore(TabsTrayState(normalTabs = listOf(mockk()), searchTermGroups = listOf(mockk())))
var result: Boolean? = null
val binding = OtherHeaderBinding(store) { result = it }
binding.start()
store.waitUntilIdle()
assertTrue(result!!)
}
}

@ -0,0 +1,72 @@
/*
* 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.browser
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.tabstray.TabsTray
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.tabstray.TabsTrayAction
import org.mozilla.fenix.tabstray.TabsTrayStore
class PrivateTabsBindingTest {
val store = TabsTrayStore()
val browserStore = BrowserStore(BrowserState(tabs = listOf(createTab("", id = "1")), selectedTabId = "1"))
val tray: TabsTray = mockk(relaxed = true)
val binding = PrivateTabsBinding(store, browserStore, tray)
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
@After
fun teardown() {
binding.stop()
}
@Test
fun `WHEN the store is updated THEN notify the tabs tray`() {
val slotTabs = slot<List<TabSessionState>>()
val expectedTabs = listOf(createTab("https://mozilla.org", private = true))
assertTrue(store.state.privateTabs.isEmpty())
store.dispatch(TabsTrayAction.UpdatePrivateTabs(expectedTabs)).joinBlocking()
binding.start()
assertTrue(store.state.privateTabs.isNotEmpty())
verify { tray.updateTabs(capture(slotTabs), "1") }
assertEquals(expectedTabs, slotTabs.captured)
}
@Test
fun `WHEN non-inactive tabs are updated THEN do not notify the tabs tray`() {
assertTrue(store.state.privateTabs.isEmpty())
store.dispatch(TabsTrayAction.UpdateInactiveTabs(listOf(createTab("https://mozilla.org", private = true))))
.joinBlocking()
binding.start()
assertTrue(store.state.privateTabs.isEmpty())
verify { tray.updateTabs(emptyList(), "1") }
}
}

@ -0,0 +1,58 @@
/* 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.browser
import mozilla.components.browser.state.state.createTab
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.tabstray.TabsTrayAction
import org.mozilla.fenix.tabstray.TabsTrayStore
class TabGroupBindingTest {
val store = TabsTrayStore()
var captured: List<TabGroup>? = null
val binding = TabGroupBinding(store) { captured = it }
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
@After
fun teardown() {
binding.stop()
}
@Test
fun `WHEN the store is updated THEN notify the adapter`() {
val expectedGroups = listOf(TabGroup("cats", emptyList(), 0))
assertTrue(store.state.searchTermGroups.isEmpty())
store.dispatch(TabsTrayAction.UpdateSearchGroupTabs(expectedGroups)).joinBlocking()
binding.start()
assertTrue(store.state.searchTermGroups.isNotEmpty())
assertEquals(expectedGroups, captured)
}
@Test
fun `WHEN non-group tabs are updated THEN do not notify the adapter`() {
assertTrue(store.state.searchTermGroups.isEmpty())
store.dispatch(TabsTrayAction.UpdatePrivateTabs(listOf(createTab("https://mozilla.org")))).joinBlocking()
binding.start()
assertTrue(store.state.searchTermGroups.isEmpty())
assertEquals(emptyList<TabGroup>(), captured)
}
}

@ -4,31 +4,18 @@
package org.mozilla.fenix.tabstray.browser
import androidx.recyclerview.widget.ConcatAdapter
import io.mockk.every
import io.mockk.mockk
import mozilla.components.browser.state.state.createTab
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.tabstray.TabsTrayStore
import org.mozilla.fenix.tabstray.TrayPagerAdapter.Companion.INACTIVE_TABS_FEATURE_NAME
import org.mozilla.fenix.tabstray.TrayPagerAdapter.Companion.TABS_TRAY_FEATURE_NAME
import org.mozilla.fenix.tabstray.TrayPagerAdapter.Companion.TAB_GROUP_FEATURE_NAME
import org.mozilla.fenix.tabstray.ext.browserAdapter
import org.mozilla.fenix.tabstray.ext.inactiveTabsAdapter
import org.mozilla.fenix.tabstray.ext.tabGroupAdapter
import org.mozilla.fenix.tabstray.ext.titleHeaderAdapter
import org.mozilla.fenix.utils.Settings
@RunWith(FenixRobolectricTestRunner::class)
class TabSorterTest {
private val context = testContext
private val settings: Settings = mockk()
private val metrics: MetricController = mockk()
private var inactiveTimestamp = 0L
@ -43,13 +30,7 @@ class TabSorterTest {
@Test
fun `WHEN updated with one normal tab THEN adapter have only one normal tab and no header`() {
val adapter = ConcatAdapter(
InactiveTabsAdapter(context, tabsTrayStore, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings),
TabGroupAdapter(context, mock(), mock(), TAB_GROUP_FEATURE_NAME),
TitleHeaderAdapter(),
BrowserTabsAdapter(context, mock(), mock(), TABS_TRAY_FEATURE_NAME)
)
val tabSorter = TabSorter(settings, metrics, adapter)
val tabSorter = TabSorter(settings, metrics, tabsTrayStore)
tabSorter.updateTabs(
listOf(
@ -58,22 +39,16 @@ class TabSorterTest {
selectedTabId = "tab1"
)
assertEquals(adapter.itemCount, 1)
assertEquals(adapter.inactiveTabsAdapter.inActiveTabsCount, 0)
assertEquals(adapter.tabGroupAdapter.itemCount, 0)
assertEquals(adapter.titleHeaderAdapter.itemCount, 0)
assertEquals(adapter.browserAdapter.itemCount, 1)
tabsTrayStore.waitUntilIdle()
assertEquals(tabsTrayStore.state.inactiveTabs.size, 0)
assertEquals(tabsTrayStore.state.searchTermGroups.size, 0)
assertEquals(tabsTrayStore.state.normalTabs.size, 1)
}
@Test
fun `WHEN updated with one normal tab and two search term tab THEN adapter have normal tab and a search group`() {
val adapter = ConcatAdapter(
InactiveTabsAdapter(context, tabsTrayStore, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings),
TabGroupAdapter(context, mock(), mock(), TAB_GROUP_FEATURE_NAME),
TitleHeaderAdapter(),
BrowserTabsAdapter(context, mock(), mock(), TABS_TRAY_FEATURE_NAME)
)
val tabSorter = TabSorter(settings, metrics, adapter)
val tabSorter = TabSorter(settings, metrics, tabsTrayStore)
tabSorter.updateTabs(
listOf(
@ -94,22 +69,16 @@ class TabSorterTest {
selectedTabId = "tab1"
)
assertEquals(adapter.itemCount, 3)
assertEquals(adapter.inactiveTabsAdapter.inActiveTabsCount, 0)
assertEquals(adapter.tabGroupAdapter.itemCount, 1)
assertEquals(adapter.titleHeaderAdapter.itemCount, 1)
assertEquals(adapter.browserAdapter.itemCount, 1)
tabsTrayStore.waitUntilIdle()
assertEquals(tabsTrayStore.state.inactiveTabs.size, 0)
assertEquals(tabsTrayStore.state.searchTermGroups.size, 1)
assertEquals(tabsTrayStore.state.normalTabs.size, 1)
}
@Test
fun `WHEN updated with one normal tab, one inactive tab and two search term tab THEN adapter have normal tab, inactive tab and a search group`() {
val adapter = ConcatAdapter(
InactiveTabsAdapter(context, tabsTrayStore, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings),
TabGroupAdapter(context, mock(), mock(), TAB_GROUP_FEATURE_NAME),
TitleHeaderAdapter(),
BrowserTabsAdapter(context, mock(), mock(), TABS_TRAY_FEATURE_NAME)
)
val tabSorter = TabSorter(settings, metrics, adapter)
val tabSorter = TabSorter(settings, metrics, tabsTrayStore)
tabSorter.updateTabs(
listOf(
@ -136,23 +105,17 @@ class TabSorterTest {
selectedTabId = "tab1"
)
assertEquals(adapter.itemCount, 4)
assertEquals(adapter.inactiveTabsAdapter.inActiveTabsCount, 1)
assertEquals(adapter.tabGroupAdapter.itemCount, 1)
assertEquals(adapter.titleHeaderAdapter.itemCount, 1)
assertEquals(adapter.browserAdapter.itemCount, 1)
tabsTrayStore.waitUntilIdle()
assertEquals(tabsTrayStore.state.inactiveTabs.size, 1)
assertEquals(tabsTrayStore.state.searchTermGroups.size, 1)
assertEquals(tabsTrayStore.state.normalTabs.size, 1)
}
@Test
fun `WHEN inactive tabs is off THEN adapter have no inactive tab`() {
every { settings.inactiveTabsAreEnabled }.answers { false }
val adapter = ConcatAdapter(
InactiveTabsAdapter(context, tabsTrayStore, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings),
TabGroupAdapter(context, mock(), mock(), TAB_GROUP_FEATURE_NAME),
TitleHeaderAdapter(),
BrowserTabsAdapter(context, mock(), mock(), TABS_TRAY_FEATURE_NAME)
)
val tabSorter = TabSorter(settings, metrics, adapter)
val tabSorter = TabSorter(settings, metrics, tabsTrayStore)
tabSorter.updateTabs(
listOf(
@ -179,23 +142,17 @@ class TabSorterTest {
selectedTabId = "tab1"
)
assertEquals(adapter.itemCount, 4)
assertEquals(adapter.inactiveTabsAdapter.inActiveTabsCount, 0)
assertEquals(adapter.tabGroupAdapter.itemCount, 1)
assertEquals(adapter.titleHeaderAdapter.itemCount, 1)
assertEquals(adapter.browserAdapter.itemCount, 2)
tabsTrayStore.waitUntilIdle()
assertEquals(tabsTrayStore.state.inactiveTabs.size, 0)
assertEquals(tabsTrayStore.state.searchTermGroups.size, 1)
assertEquals(tabsTrayStore.state.normalTabs.size, 2)
}
@Test
fun `WHEN search term tabs is off THEN adapter have no search term group`() {
every { settings.searchTermTabGroupsAreEnabled }.answers { false }
val adapter = ConcatAdapter(
InactiveTabsAdapter(context, tabsTrayStore, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings),
TabGroupAdapter(context, mock(), mock(), TAB_GROUP_FEATURE_NAME),
TitleHeaderAdapter(),
BrowserTabsAdapter(context, mock(), mock(), TABS_TRAY_FEATURE_NAME)
)
val tabSorter = TabSorter(settings, metrics, adapter)
val tabSorter = TabSorter(settings, metrics, tabsTrayStore)
tabSorter.updateTabs(
listOf(
@ -222,24 +179,18 @@ class TabSorterTest {
selectedTabId = "tab1"
)
assertEquals(adapter.itemCount, 4)
assertEquals(adapter.inactiveTabsAdapter.inActiveTabsCount, 1)
assertEquals(adapter.tabGroupAdapter.itemCount, 0)
assertEquals(adapter.titleHeaderAdapter.itemCount, 0)
assertEquals(adapter.browserAdapter.itemCount, 3)
tabsTrayStore.waitUntilIdle()
assertEquals(tabsTrayStore.state.inactiveTabs.size, 1)
assertEquals(tabsTrayStore.state.searchTermGroups.size, 0)
assertEquals(tabsTrayStore.state.normalTabs.size, 3)
}
@Test
fun `WHEN both inactive tabs and search term tabs are off THEN adapter have only normal tabs`() {
every { settings.inactiveTabsAreEnabled }.answers { false }
every { settings.searchTermTabGroupsAreEnabled }.answers { false }
val adapter = ConcatAdapter(
InactiveTabsAdapter(context, tabsTrayStore, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings),
TabGroupAdapter(context, mock(), mock(), TAB_GROUP_FEATURE_NAME),
TitleHeaderAdapter(),
BrowserTabsAdapter(context, mock(), mock(), TABS_TRAY_FEATURE_NAME)
)
val tabSorter = TabSorter(settings, metrics, adapter)
val tabSorter = TabSorter(settings, metrics, tabsTrayStore)
tabSorter.updateTabs(
listOf(
@ -265,22 +216,16 @@ class TabSorterTest {
selectedTabId = "tab1"
)
assertEquals(adapter.itemCount, 4)
assertEquals(adapter.inactiveTabsAdapter.inActiveTabsCount, 0)
assertEquals(adapter.tabGroupAdapter.itemCount, 0)
assertEquals(adapter.titleHeaderAdapter.itemCount, 0)
assertEquals(adapter.browserAdapter.itemCount, 4)
tabsTrayStore.waitUntilIdle()
assertEquals(tabsTrayStore.state.inactiveTabs.size, 0)
assertEquals(tabsTrayStore.state.searchTermGroups.size, 0)
assertEquals(tabsTrayStore.state.normalTabs.size, 4)
}
@Test
fun `WHEN only one search term tab THEN there is no search group`() {
val adapter = ConcatAdapter(
InactiveTabsAdapter(context, tabsTrayStore, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings),
TabGroupAdapter(context, mock(), mock(), TAB_GROUP_FEATURE_NAME),
TitleHeaderAdapter(),
BrowserTabsAdapter(context, mock(), mock(), TABS_TRAY_FEATURE_NAME)
)
val tabSorter = TabSorter(settings, metrics, adapter)
val tabSorter = TabSorter(settings, metrics, tabsTrayStore)
tabSorter.updateTabs(
listOf(
@ -292,22 +237,16 @@ class TabSorterTest {
selectedTabId = "tab1"
)
assertEquals(adapter.itemCount, 1)
assertEquals(adapter.inactiveTabsAdapter.inActiveTabsCount, 0)
assertEquals(adapter.tabGroupAdapter.itemCount, 0)
assertEquals(adapter.titleHeaderAdapter.itemCount, 0)
assertEquals(adapter.browserAdapter.itemCount, 1)
tabsTrayStore.waitUntilIdle()
assertEquals(tabsTrayStore.state.inactiveTabs.size, 0)
assertEquals(tabsTrayStore.state.searchTermGroups.size, 0)
assertEquals(tabsTrayStore.state.normalTabs.size, 1)
}
@Test
fun `WHEN remove second last one search term tab THEN search group is kept even if there's only one tab`() {
val adapter = ConcatAdapter(
InactiveTabsAdapter(context, tabsTrayStore, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings),
TabGroupAdapter(context, mock(), mock(), TAB_GROUP_FEATURE_NAME),
TitleHeaderAdapter(),
BrowserTabsAdapter(context, mock(), mock(), TABS_TRAY_FEATURE_NAME)
)
val tabSorter = TabSorter(settings, metrics, adapter)
val tabSorter = TabSorter(settings, metrics, tabsTrayStore)
tabSorter.updateTabs(
listOf(
@ -323,11 +262,11 @@ class TabSorterTest {
selectedTabId = "tab1"
)
assertEquals(adapter.itemCount, 1)
assertEquals(adapter.inactiveTabsAdapter.inActiveTabsCount, 0)
assertEquals(adapter.tabGroupAdapter.itemCount, 1)
assertEquals(adapter.titleHeaderAdapter.itemCount, 0)
assertEquals(adapter.browserAdapter.itemCount, 0)
tabsTrayStore.waitUntilIdle()
assertEquals(tabsTrayStore.state.inactiveTabs.size, 0)
assertEquals(tabsTrayStore.state.searchTermGroups.size, 1)
assertEquals(tabsTrayStore.state.normalTabs.size, 0)
tabSorter.updateTabs(
listOf(
@ -339,10 +278,10 @@ class TabSorterTest {
selectedTabId = "tab1"
)
assertEquals(adapter.itemCount, 1)
assertEquals(adapter.inactiveTabsAdapter.inActiveTabsCount, 0)
assertEquals(adapter.tabGroupAdapter.itemCount, 1)
assertEquals(adapter.titleHeaderAdapter.itemCount, 0)
assertEquals(adapter.browserAdapter.itemCount, 0)
tabsTrayStore.waitUntilIdle()
assertEquals(tabsTrayStore.state.inactiveTabs.size, 0)
assertEquals(tabsTrayStore.state.searchTermGroups.size, 1)
assertEquals(tabsTrayStore.state.normalTabs.size, 0)
}
}

Loading…
Cancel
Save