mirror of
https://github.com/fork-maintainers/iceraven-browser
synced 2024-11-15 18:12:54 +00:00
[fenix] For https://github.com/mozilla-mobile/fenix/issues/19933 - Show a media tab item on homescreen for the last tab with media
This commit is contained in:
parent
e671e5314e
commit
e469026805
@ -33,6 +33,7 @@ import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
|
||||
import mozilla.components.feature.downloads.DownloadMiddleware
|
||||
import mozilla.components.feature.logins.exceptions.LoginExceptionStorage
|
||||
import mozilla.components.feature.media.MediaSessionFeature
|
||||
import mozilla.components.feature.media.middleware.LastMediaAccessMiddleware
|
||||
import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware
|
||||
import mozilla.components.feature.prompts.PromptMiddleware
|
||||
import mozilla.components.feature.pwa.ManifestStorage
|
||||
@ -208,7 +209,8 @@ class Core(
|
||||
),
|
||||
RecordingDevicesMiddleware(context),
|
||||
PromptMiddleware(),
|
||||
AdsTelemetryMiddleware(adsTelemetry)
|
||||
AdsTelemetryMiddleware(adsTelemetry),
|
||||
LastMediaAccessMiddleware()
|
||||
)
|
||||
|
||||
if (FeatureFlags.historyMetadataFeature) {
|
||||
|
@ -4,21 +4,40 @@
|
||||
|
||||
package org.mozilla.fenix.ext
|
||||
|
||||
import mozilla.components.browser.state.selector.selectedTab
|
||||
import mozilla.components.browser.state.selector.normalTabs
|
||||
import mozilla.components.browser.state.selector.selectedNormalTab
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
|
||||
/**
|
||||
* Returns the currently selected tab if there's one as a list.
|
||||
* Get the last opened normal tab and the last tab with in progress media, if available.
|
||||
*
|
||||
* @return A list of the currently selected tab or an empty list.
|
||||
* @return A list of the last opened tab and the last tab with in progress media
|
||||
* if distinct and available or an empty list.
|
||||
*/
|
||||
fun BrowserState.asRecentTabs(): List<TabSessionState> {
|
||||
val tab = selectedTab
|
||||
return mutableListOf<TabSessionState>().apply {
|
||||
val lastOpenedNormalTab = lastOpenedNormalTab
|
||||
lastOpenedNormalTab?.let { add(it) }
|
||||
inProgressMediaTab
|
||||
?.takeUnless { it == lastOpenedNormalTab }
|
||||
?.let {
|
||||
add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return if (tab != null && !tab.content.private) {
|
||||
listOfNotNull(tab)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get the selected normal tab or the last accessed normal tab
|
||||
* if there is no selected tab or the selected tab is a private one.
|
||||
*/
|
||||
val BrowserState.lastOpenedNormalTab: TabSessionState?
|
||||
get() = selectedNormalTab ?: normalTabs.maxByOrNull { it.lastAccess }
|
||||
|
||||
/**
|
||||
* Get the last tab with in progress media.
|
||||
*/
|
||||
val BrowserState.inProgressMediaTab: TabSessionState?
|
||||
get() = normalTabs
|
||||
.filter { it.lastMediaAccess > 0 }
|
||||
.maxByOrNull { it.lastMediaAccess }
|
||||
|
@ -7,14 +7,13 @@ package org.mozilla.fenix.home.recenttabs
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.map
|
||||
import mozilla.components.browser.state.selector.normalTabs
|
||||
import mozilla.components.browser.state.selector.selectedNormalTab
|
||||
import mozilla.components.browser.state.selector.selectedTab
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.lib.state.helpers.AbstractBinding
|
||||
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
|
||||
import org.mozilla.fenix.ext.asRecentTabs
|
||||
import org.mozilla.fenix.ext.lastOpenedNormalTab
|
||||
import org.mozilla.fenix.home.HomeFragmentAction
|
||||
import org.mozilla.fenix.home.HomeFragmentStore
|
||||
|
||||
@ -29,20 +28,20 @@ class RecentTabsListFeature(
|
||||
) : AbstractBinding<BrowserState>(browserStore) {
|
||||
|
||||
override suspend fun onState(flow: Flow<BrowserState>) {
|
||||
flow.map { it.selectedTab }
|
||||
.ifAnyChanged { arrayOf(it?.id, it?.content?.title, it?.content?.icon) }
|
||||
.collect { _ ->
|
||||
// Attempt to get the selected normal tab or the last accessed normal tab
|
||||
// if there is no selected tab or the selected tab is a private one.
|
||||
val selectedTab = browserStore.state.selectedNormalTab
|
||||
?: browserStore.state.normalTabs.maxByOrNull { it.lastAccess }
|
||||
val recentTabsList = if (selectedTab != null) {
|
||||
listOf(selectedTab)
|
||||
} else {
|
||||
emptyList()
|
||||
flow
|
||||
// Listen for changes regarding the currently selected tab and the in progress media tab
|
||||
// and also for changes (close, undo) in normal tabs that could involve these.
|
||||
.ifAnyChanged {
|
||||
val lastOpenedNormalTab = it.lastOpenedNormalTab
|
||||
arrayOf(
|
||||
lastOpenedNormalTab?.id,
|
||||
lastOpenedNormalTab?.content?.title,
|
||||
lastOpenedNormalTab?.content?.icon,
|
||||
it.normalTabs
|
||||
)
|
||||
}
|
||||
|
||||
homeStore.dispatch(HomeFragmentAction.RecentTabsChange(recentTabsList))
|
||||
.collect {
|
||||
homeStore.dispatch(HomeFragmentAction.RecentTabsChange(browserStore.state.asRecentTabs()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,132 @@
|
||||
/* 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.recenttabs.view
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import mozilla.components.support.ktx.android.content.getColorFromAttr
|
||||
import mozilla.components.support.ktx.android.util.dpToPx
|
||||
import org.mozilla.fenix.R
|
||||
|
||||
private const val TOP_MARGIN_DP = 1
|
||||
|
||||
/**
|
||||
* All possible positions of a recent tab in relation to others when shown in the "Jump back in" section.
|
||||
*/
|
||||
enum class RecentTabsItemPosition {
|
||||
/**
|
||||
* This is the only tab to be shown in this section.
|
||||
*/
|
||||
SINGLE,
|
||||
|
||||
/**
|
||||
* This item is to be shown at the top of the section with others below it.
|
||||
*/
|
||||
TOP,
|
||||
|
||||
/**
|
||||
* This item is to be shown between others in this section.
|
||||
*/
|
||||
MIDDLE,
|
||||
|
||||
/**
|
||||
* This item is to be shown at the bottom of the section with others above it.
|
||||
*/
|
||||
BOTTOM
|
||||
}
|
||||
|
||||
/**
|
||||
* Helpers for setting various layout properties for the view from a [RecentTabViewHolder].
|
||||
*
|
||||
* Depending on the provided [RecentTabsItemPosition]:
|
||||
* - sets a different background so that the entire section possibly containing
|
||||
* more such items would have rounded corners but sibling items not.
|
||||
* - sets small margins for the items so that there's a clear separation between siblings
|
||||
*/
|
||||
sealed class RecentTabViewDecorator {
|
||||
/**
|
||||
* Apply the decoration to [itemView].
|
||||
*/
|
||||
abstract operator fun invoke(itemView: View): View
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Get the appropriate decorator to set view background / margins depending on the position
|
||||
* of that view in the recent tabs section.
|
||||
*/
|
||||
fun forPosition(position: RecentTabsItemPosition) = when (position) {
|
||||
RecentTabsItemPosition.SINGLE -> SingleTabDecoration
|
||||
RecentTabsItemPosition.TOP -> TopTabDecoration
|
||||
RecentTabsItemPosition.MIDDLE -> MiddleTabDecoration
|
||||
RecentTabsItemPosition.BOTTOM -> BottomTabDecoration
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorator for a view shown in the recent tabs section that will update it to express
|
||||
* that that item is the single one shown in this section.
|
||||
*/
|
||||
object SingleTabDecoration : RecentTabViewDecorator() {
|
||||
override fun invoke(itemView: View): View {
|
||||
val context = itemView.context
|
||||
|
||||
itemView.background =
|
||||
AppCompatResources.getDrawable(context, R.drawable.home_list_row_background)
|
||||
|
||||
return itemView
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorator for a view shown in the recent tabs section that will update it to express
|
||||
* that this is an item shown at the top of the section and there are others below it.
|
||||
*/
|
||||
object TopTabDecoration : RecentTabViewDecorator() {
|
||||
override fun invoke(itemView: View): View {
|
||||
val context = itemView.context
|
||||
|
||||
itemView.background =
|
||||
AppCompatResources.getDrawable(context, R.drawable.rounded_top_corners)
|
||||
|
||||
return itemView
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorator for a view shown in the recent tabs section that will update it to express
|
||||
* that this is an item shown has other recents tabs to be shown on top or below it.
|
||||
*/
|
||||
object MiddleTabDecoration : RecentTabViewDecorator() {
|
||||
override fun invoke(itemView: View): View {
|
||||
val context = itemView.context
|
||||
|
||||
itemView.setBackgroundColor(context.getColorFromAttr(R.attr.above))
|
||||
|
||||
(itemView.layoutParams as? ViewGroup.MarginLayoutParams)?.topMargin =
|
||||
TOP_MARGIN_DP.dpToPx(context.resources.displayMetrics)
|
||||
|
||||
return itemView
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorator for a view shown in the recent tabs section that will update it to express
|
||||
* that this is an item shown at the bottom of the section and there are others above it.
|
||||
*/
|
||||
object BottomTabDecoration : RecentTabViewDecorator() {
|
||||
override fun invoke(itemView: View): View {
|
||||
val context = itemView.context
|
||||
|
||||
itemView.background =
|
||||
AppCompatResources.getDrawable(context, R.drawable.rounded_bottom_corners)
|
||||
|
||||
(itemView.layoutParams as? ViewGroup.MarginLayoutParams)?.topMargin =
|
||||
TOP_MARGIN_DP.dpToPx(context.resources.displayMetrics)
|
||||
|
||||
return itemView
|
||||
}
|
||||
}
|
||||
}
|
@ -28,7 +28,7 @@ class RecentTabViewHolder(
|
||||
private val icons: BrowserIcons = view.context.components.core.icons
|
||||
) : ViewHolder(view) {
|
||||
|
||||
fun bindTab(tab: TabSessionState) {
|
||||
fun bindTab(tab: TabSessionState): View {
|
||||
// A page may take a while to retrieve a title, so let's show the url until we get one.
|
||||
recent_tab_title.text = if (tab.content.title.isNotEmpty()) {
|
||||
tab.content.title
|
||||
@ -46,6 +46,8 @@ class RecentTabViewHolder(
|
||||
itemView.setOnClickListener {
|
||||
interactor.onRecentTabClicked(tab.id)
|
||||
}
|
||||
|
||||
return itemView
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -20,6 +20,7 @@ import mozilla.components.ui.widgets.WidgetSiteItemView
|
||||
import org.mozilla.fenix.components.Components
|
||||
import org.mozilla.fenix.components.tips.Tip
|
||||
import org.mozilla.fenix.home.OnboardingState
|
||||
import org.mozilla.fenix.home.recenttabs.view.RecentTabViewDecorator
|
||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionHeaderViewHolder
|
||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
|
||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.NoCollectionsMessageViewHolder
|
||||
@ -41,6 +42,7 @@ import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingWh
|
||||
import org.mozilla.fenix.home.recenttabs.view.RecentTabViewHolder
|
||||
import org.mozilla.fenix.home.recenttabs.view.RecentTabsHeaderViewHolder
|
||||
import org.mozilla.fenix.home.recentbookmarks.view.RecentBookmarksViewHolder
|
||||
import org.mozilla.fenix.home.recenttabs.view.RecentTabsItemPosition
|
||||
import org.mozilla.fenix.home.tips.ButtonTipViewHolder
|
||||
import mozilla.components.feature.tab.collections.Tab as ComponentTab
|
||||
|
||||
@ -141,8 +143,12 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
|
||||
object OnboardingWhatsNew : AdapterItem(OnboardingWhatsNewViewHolder.LAYOUT_ID)
|
||||
|
||||
object RecentTabsHeader : AdapterItem(RecentTabsHeaderViewHolder.LAYOUT_ID)
|
||||
data class RecentTabItem(val tab: TabSessionState) : AdapterItem(RecentTabViewHolder.LAYOUT_ID) {
|
||||
override fun sameAs(other: AdapterItem) = other is RecentTabItem && tab.id == other.tab.id
|
||||
data class RecentTabItem(
|
||||
val tab: TabSessionState,
|
||||
val position: RecentTabsItemPosition
|
||||
) : AdapterItem(RecentTabViewHolder.LAYOUT_ID) {
|
||||
override fun sameAs(other: AdapterItem) = other is RecentTabItem && tab.id == other.tab.id &&
|
||||
position == other.position
|
||||
|
||||
override fun contentsSameAs(other: AdapterItem): Boolean {
|
||||
val otherItem = other as RecentTabItem
|
||||
@ -316,7 +322,10 @@ class SessionControlAdapter(
|
||||
(item as AdapterItem.OnboardingAutomaticSignIn).state.withAccount
|
||||
)
|
||||
is RecentTabViewHolder -> {
|
||||
holder.bindTab((item as AdapterItem.RecentTabItem).tab)
|
||||
val (tab, tabPosition) = item as AdapterItem.RecentTabItem
|
||||
holder.bindTab(tab).apply {
|
||||
RecentTabViewDecorator.forPosition(tabPosition).invoke(this)
|
||||
}
|
||||
}
|
||||
is RecentBookmarksViewHolder -> {
|
||||
holder.bind(
|
||||
|
@ -5,6 +5,7 @@
|
||||
package org.mozilla.fenix.home.sessioncontrol
|
||||
|
||||
import android.view.View
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@ -20,6 +21,7 @@ import org.mozilla.fenix.home.HomeFragmentState
|
||||
import org.mozilla.fenix.home.HomeScreenViewModel
|
||||
import org.mozilla.fenix.home.Mode
|
||||
import org.mozilla.fenix.home.OnboardingState
|
||||
import org.mozilla.fenix.home.recenttabs.view.RecentTabsItemPosition
|
||||
|
||||
// This method got a little complex with the addition of the tab tray feature flag
|
||||
// When we remove the tabs from the home screen this will get much simpler again.
|
||||
@ -65,13 +67,42 @@ private fun normalModeAdapterItems(
|
||||
return items
|
||||
}
|
||||
|
||||
private fun showRecentTabs(
|
||||
/**
|
||||
* Constructs the list of items to be shown in the recent tabs section.
|
||||
*
|
||||
* This section's structure is:
|
||||
* - section header
|
||||
* - one or more normal tabs
|
||||
* - zero or one media tab (if there is a tab opened on which media started playing.
|
||||
* This may be a duplicate of one of the normal tabs shown above).
|
||||
*/
|
||||
@VisibleForTesting
|
||||
internal fun showRecentTabs(
|
||||
recentTabs: List<TabSessionState>,
|
||||
items: MutableList<AdapterItem>
|
||||
) {
|
||||
items.add(AdapterItem.RecentTabsHeader)
|
||||
recentTabs.forEach {
|
||||
items.add(AdapterItem.RecentTabItem(it))
|
||||
|
||||
recentTabs.forEachIndexed { index, recentTab ->
|
||||
// If this is the first tab to be shown but more will follow.
|
||||
if (index == 0 && recentTabs.size > 1) {
|
||||
items.add(AdapterItem.RecentTabItem(recentTab, RecentTabsItemPosition.TOP))
|
||||
}
|
||||
|
||||
// if this is the only tab to be shown.
|
||||
else if (index == 0 && recentTabs.size == 1) {
|
||||
items.add(AdapterItem.RecentTabItem(recentTab, RecentTabsItemPosition.SINGLE))
|
||||
}
|
||||
|
||||
// If there are items above and below.
|
||||
else if (index < recentTabs.size - 1) {
|
||||
items.add(AdapterItem.RecentTabItem(recentTab, RecentTabsItemPosition.MIDDLE))
|
||||
}
|
||||
|
||||
// If this is the last recent tab to be shown.
|
||||
else if (index < recentTabs.size) {
|
||||
items.add(AdapterItem.RecentTabItem(recentTab, RecentTabsItemPosition.BOTTOM))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:background="@drawable/home_list_row_background"
|
||||
android:clipToPadding="false"
|
||||
android:elevation="@dimen/home_item_elevation"
|
||||
android:foreground="?android:attr/selectableItemBackground">
|
||||
|
@ -4,61 +4,146 @@
|
||||
|
||||
package org.mozilla.fenix.ext
|
||||
|
||||
import io.mockk.mockk
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.state.createTab
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class BrowserStateTest {
|
||||
|
||||
@Test
|
||||
fun `WHEN there is a selected tab THEN asRecentTabs returns the selected tab as a list`() {
|
||||
val tab = createTab(
|
||||
url = "https://www.mozilla.org",
|
||||
id = "1"
|
||||
fun `GIVEN a tab which had media playing WHEN inProgressMediaTab is called THEN return that tab`() {
|
||||
val inProgressMediaTab = createTab(url = "mediaUrl", id = "2", lastMediaAccess = 123)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(mockk(relaxed = true), inProgressMediaTab, mockk(relaxed = true))
|
||||
)
|
||||
val tabs = listOf(tab)
|
||||
val state = BrowserState(
|
||||
tabs = tabs,
|
||||
selectedTabId = tab.id
|
||||
)
|
||||
val recentTabs = state.asRecentTabs()
|
||||
|
||||
assertEquals(tabs, recentTabs)
|
||||
assertEquals(inProgressMediaTab, browserState.inProgressMediaTab)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN there is no selected tab THEN asRecentTabs returns an empty list`() {
|
||||
val state = BrowserState(
|
||||
tabs = listOf(
|
||||
createTab(
|
||||
url = "https://www.mozilla.org",
|
||||
id = "1"
|
||||
fun `GIVEN no tab which had media playing exists WHEN inProgressMediaTab is called THEN return null`() {
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(mockk(relaxed = true), mockk(relaxed = true), mockk(relaxed = true))
|
||||
)
|
||||
)
|
||||
)
|
||||
val recentTabs = state.asRecentTabs()
|
||||
|
||||
assertEquals(0, recentTabs.size)
|
||||
assertNull(browserState.inProgressMediaTab)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the selected tab is private THEN asRecentTabs returns an empty list`() {
|
||||
val tab = createTab(
|
||||
url = "https://www.mozilla.org",
|
||||
id = "1",
|
||||
private = true
|
||||
fun `GIVEN the selected tab is a normal tab and no media tab WHEN asRecentTabs is called THEN return a list of that tab`() {
|
||||
val selectedTab = createTab(url = "url", id = "3")
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(mockk(relaxed = true), selectedTab, mockk(relaxed = true)),
|
||||
selectedTabId = selectedTab.id
|
||||
)
|
||||
val tabs = listOf(tab)
|
||||
val state = BrowserState(
|
||||
tabs = tabs,
|
||||
selectedTabId = tab.id
|
||||
)
|
||||
val recentTabs = state.asRecentTabs()
|
||||
|
||||
assertEquals(0, recentTabs.size)
|
||||
val result = browserState.asRecentTabs()
|
||||
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(selectedTab, result[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the selected tab is a private tab and no media tab WHEN asRecentTabs is called THEN return a list of the last accessed normal tab`() {
|
||||
val selectedPrivateTab = createTab(url = "url", id = "1", lastAccess = 1, private = true)
|
||||
val lastAccessedNormalTab = createTab(url = "url2", id = "2", lastAccess = 2)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(mockk(relaxed = true), lastAccessedNormalTab, selectedPrivateTab),
|
||||
selectedTabId = selectedPrivateTab.id
|
||||
)
|
||||
|
||||
val result = browserState.asRecentTabs()
|
||||
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(lastAccessedNormalTab, result[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the selected tab is a normal tab and another media tab exists WHEN asRecentTabs is called THEN return a list of these tabs`() {
|
||||
val selectedTab = createTab(url = "url", id = "3")
|
||||
val mediaTab = createTab("mediaUrl", id = "23", lastMediaAccess = 123)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(mockk(relaxed = true), selectedTab, mediaTab),
|
||||
selectedTabId = selectedTab.id
|
||||
)
|
||||
|
||||
val result = browserState.asRecentTabs()
|
||||
|
||||
assertEquals(2, result.size)
|
||||
assertEquals(selectedTab, result[0])
|
||||
assertEquals(mediaTab, result[1])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the selected tab is a private tab and another media tab exists WHEN asRecentTabs is called THEN return a list of the last normal tab and the media tab`() {
|
||||
val lastAccessedNormalTab = createTab(url = "url2", id = "2", lastAccess = 2)
|
||||
val selectedPrivateTab = createTab(url = "url", id = "1", lastAccess = 1, private = true)
|
||||
val mediaTab = createTab("mediaUrl", id = "12", lastAccess = 0, lastMediaAccess = 123)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(mockk(relaxed = true), lastAccessedNormalTab, selectedPrivateTab, mediaTab),
|
||||
selectedTabId = selectedPrivateTab.id
|
||||
)
|
||||
|
||||
val result = browserState.asRecentTabs()
|
||||
|
||||
assertEquals(2, result.size)
|
||||
assertEquals(lastAccessedNormalTab, result[0])
|
||||
assertEquals(mediaTab, result[1])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the selected tab is a private tab and the media tab is the last accessed normal tab WHEN asRecentTabs is called THEN a list of the media tab`() {
|
||||
val selectedPrivateTab = createTab(url = "url", id = "1", lastAccess = 1, private = true)
|
||||
val normalTab = createTab(url = "url2", id = "2", lastAccess = 2)
|
||||
val mediaTab = createTab("mediaUrl", id = "12", lastAccess = 20, lastMediaAccess = 123)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(mockk(relaxed = true), normalTab, selectedPrivateTab, mediaTab),
|
||||
selectedTabId = selectedPrivateTab.id
|
||||
)
|
||||
|
||||
val result = browserState.asRecentTabs()
|
||||
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(mediaTab, result[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN only private tabs and a private one selected WHEN lastOpenedNormalTab is called THEN return null`() {
|
||||
val selectedPrivateTab = createTab(url = "url", id = "1", private = true)
|
||||
val otherPrivateTab = createTab(url = "url2", id = "2", private = true)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(selectedPrivateTab, otherPrivateTab),
|
||||
selectedTabId = "1"
|
||||
)
|
||||
|
||||
assertNull(browserState.lastOpenedNormalTab)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN normal tabs exists but a private one is selected WHEN lastOpenedNormalTab is called THEN return the last accessed normal tab`() {
|
||||
val selectedPrivateTab = createTab(url = "url", id = "1", private = true)
|
||||
val normalTab1 = createTab(url = "url2", id = "2", private = false, lastAccess = 2)
|
||||
val normalTab2 = createTab(url = "url3", id = "3", private = false, lastAccess = 3)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(selectedPrivateTab, normalTab1, normalTab2),
|
||||
selectedTabId = "3"
|
||||
)
|
||||
|
||||
assertEquals(normalTab2, browserState.lastOpenedNormalTab)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a normal tab is selected WHEN lastOpenedNormalTab is called THEN return the selected normal tab`() {
|
||||
val normalTab1 = createTab(url = "url1", id = "1", private = false)
|
||||
val normalTab2 = createTab(url = "url2", id = "2", private = false)
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(normalTab1, normalTab2),
|
||||
selectedTabId = "1"
|
||||
)
|
||||
|
||||
assertEquals(normalTab1, browserState.lastOpenedNormalTab)
|
||||
}
|
||||
}
|
||||
|
@ -9,10 +9,13 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestCoroutineDispatcher
|
||||
import mozilla.components.browser.state.action.ContentAction.UpdateIconAction
|
||||
import mozilla.components.browser.state.action.ContentAction.UpdateTitleAction
|
||||
import mozilla.components.browser.state.action.MediaSessionAction
|
||||
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.concept.engine.mediasession.MediaSession
|
||||
import mozilla.components.feature.media.middleware.LastMediaAccessMiddleware
|
||||
import mozilla.components.support.test.ext.joinBlocking
|
||||
import mozilla.components.support.test.libstate.ext.waitUntilIdle
|
||||
import mozilla.components.support.test.middleware.CaptureActionsMiddleware
|
||||
@ -49,7 +52,7 @@ class RecentTabsListFeatureTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no selected or last active tab WHEN the feature starts THEN dispatch an empty list`() {
|
||||
fun `GIVEN no selected, last active or in progress media tab WHEN the feature starts THEN dispatch an empty list`() {
|
||||
val browserStore = BrowserStore()
|
||||
val homeStore = HomeFragmentStore()
|
||||
val feature = RecentTabsListFeature(
|
||||
@ -111,6 +114,46 @@ class RecentTabsListFeatureTest {
|
||||
assertEquals(1, homeStore.state.recentTabs.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a valid inProgressMediaTabId and another selected tab exists WHEN the feature starts THEN dispatch both as as a recent tabs list`() {
|
||||
val mediaTab = createTab("https://mozilla.com", id = "42", lastMediaAccess = 123)
|
||||
val selectedTab = createTab("https://mozilla.com", id = "43")
|
||||
val browserStore = BrowserStore(BrowserState(
|
||||
tabs = listOf(mediaTab, selectedTab),
|
||||
selectedTabId = "43"
|
||||
))
|
||||
val feature = RecentTabsListFeature(
|
||||
browserStore = browserStore,
|
||||
homeStore = homeStore
|
||||
)
|
||||
|
||||
feature.start()
|
||||
homeStore.waitUntilIdle()
|
||||
|
||||
assertEquals(2, homeStore.state.recentTabs.size)
|
||||
assertEquals(selectedTab, homeStore.state.recentTabs[0])
|
||||
assertEquals(mediaTab, homeStore.state.recentTabs[1])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a valid inProgressMediaTabId exists and that is the selected tab WHEN the feature starts THEN dispatch just one tab as the recent tabs list`() {
|
||||
val selectedMediaTab = createTab("https://mozilla.com", id = "42", lastMediaAccess = 123)
|
||||
val browserStore = BrowserStore(BrowserState(
|
||||
tabs = listOf(selectedMediaTab),
|
||||
selectedTabId = "42"
|
||||
))
|
||||
val feature = RecentTabsListFeature(
|
||||
browserStore = browserStore,
|
||||
homeStore = homeStore
|
||||
)
|
||||
|
||||
feature.start()
|
||||
homeStore.waitUntilIdle()
|
||||
|
||||
assertEquals(1, homeStore.state.recentTabs.size)
|
||||
assertEquals(selectedMediaTab, homeStore.state.recentTabs[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the browser state has an updated select tab THEN dispatch the new recent tab list`() {
|
||||
val tab1 = createTab(
|
||||
@ -148,6 +191,40 @@ class RecentTabsListFeatureTest {
|
||||
assertEquals(tab2, homeStore.state.recentTabs[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the browser state has an in progress media tab THEN dispatch the new recent tab list`() {
|
||||
val initialMediaTab = createTab(url = "https://mozilla.com", id = "1", lastMediaAccess = 123)
|
||||
val newMediaTab = createTab(url = "http://mozilla.org", id = "2", lastMediaAccess = 100)
|
||||
val browserStore = BrowserStore(
|
||||
initialState = BrowserState(
|
||||
tabs = listOf(initialMediaTab, newMediaTab),
|
||||
selectedTabId = "1"
|
||||
),
|
||||
middleware = listOf(LastMediaAccessMiddleware())
|
||||
)
|
||||
val feature = RecentTabsListFeature(
|
||||
browserStore = browserStore,
|
||||
homeStore = homeStore
|
||||
)
|
||||
|
||||
feature.start()
|
||||
homeStore.waitUntilIdle()
|
||||
assertEquals(1, homeStore.state.recentTabs.size)
|
||||
assertEquals(initialMediaTab, homeStore.state.recentTabs[0])
|
||||
|
||||
browserStore.dispatch(
|
||||
MediaSessionAction.UpdateMediaPlaybackStateAction("2", MediaSession.PlaybackState.PLAYING)
|
||||
).joinBlocking()
|
||||
homeStore.waitUntilIdle()
|
||||
assertEquals(2, homeStore.state.recentTabs.size)
|
||||
assertEquals(initialMediaTab, homeStore.state.recentTabs[0])
|
||||
// UpdateMediaPlaybackStateAction would set the current timestamp as the new value for lastMediaAccess
|
||||
val updatedLastMediaAccess = homeStore.state.recentTabs[1].lastMediaAccess
|
||||
assertTrue("expected lastMediaAccess ($updatedLastMediaAccess) > 100", updatedLastMediaAccess > 100)
|
||||
// Check that the media tab is updated ignoring just the lastMediaAccess property.
|
||||
assertEquals(newMediaTab, homeStore.state.recentTabs[1].copy(lastMediaAccess = 100))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the browser state selects a private tab THEN dispatch an empty list`() {
|
||||
val selectedNormalTab = createTab(
|
||||
@ -242,4 +319,25 @@ class RecentTabsListFeatureTest {
|
||||
assertNotNull(tab.content.icon)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN inProgressMediaTab already set WHEN the media tab is closed THEN remove it from recent tabs`() {
|
||||
val initialMediaTab = createTab(url = "https://mozilla.com", id = "1")
|
||||
val selectedTab = createTab(url = "https://mozilla.com/firefox", id = "2")
|
||||
val browserStore = BrowserStore(
|
||||
initialState = BrowserState(listOf(initialMediaTab, selectedTab), selectedTabId = "2"),
|
||||
middleware = listOf(LastMediaAccessMiddleware())
|
||||
)
|
||||
val feature = RecentTabsListFeature(
|
||||
browserStore = browserStore,
|
||||
homeStore = homeStore
|
||||
)
|
||||
|
||||
feature.start()
|
||||
browserStore.dispatch(TabListAction.RemoveTabsAction(listOf("1"))).joinBlocking()
|
||||
homeStore.waitUntilIdle()
|
||||
|
||||
assertEquals(1, homeStore.state.recentTabs.size)
|
||||
assertEquals(selectedTab, homeStore.state.recentTabs[0])
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,142 @@
|
||||
/* 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.recenttabs.view
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.slot
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import mozilla.components.support.ktx.android.content.getColorFromAttr
|
||||
import mozilla.components.support.ktx.android.util.dpToPx
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.R
|
||||
|
||||
class RecentTabViewDecoratorTest {
|
||||
@Test
|
||||
fun `WHEN forPosition is called with RecentTabsItemPosition#SINGLE THEN return SingleTabDecoration`() {
|
||||
val result = RecentTabViewDecorator.forPosition(RecentTabsItemPosition.SINGLE)
|
||||
|
||||
assertTrue(result is RecentTabViewDecorator.SingleTabDecoration)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN forPosition is called with RecentTabsItemPosition#TOP THEN return TopTabDecoration`() {
|
||||
val result = RecentTabViewDecorator.forPosition(RecentTabsItemPosition.TOP)
|
||||
|
||||
assertTrue(result is RecentTabViewDecorator.TopTabDecoration)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN forPosition is called with RecentTabsItemPosition#MIDDLE THEN return MiddleTabDecoration`() {
|
||||
val result = RecentTabViewDecorator.forPosition(RecentTabsItemPosition.MIDDLE)
|
||||
|
||||
assertTrue(result is RecentTabViewDecorator.MiddleTabDecoration)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN forPosition is called with RecentTabsItemPosition#BOTTOM THEN return SingleTabDecoration`() {
|
||||
val result = RecentTabViewDecorator.forPosition(RecentTabsItemPosition.BOTTOM)
|
||||
|
||||
assertTrue(result is RecentTabViewDecorator.BottomTabDecoration)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN SingleTabDecoration is invoked for a View THEN set the appropriate background`() {
|
||||
val view: View = mockk(relaxed = true)
|
||||
val drawable: Drawable = mockk()
|
||||
val drawableResCaptor = slot<Int>()
|
||||
|
||||
try {
|
||||
mockkStatic(AppCompatResources::class)
|
||||
every { AppCompatResources.getDrawable(any(), capture(drawableResCaptor)) } returns drawable
|
||||
|
||||
RecentTabViewDecorator.SingleTabDecoration(view)
|
||||
|
||||
verify { view.background = drawable }
|
||||
assertEquals(R.drawable.home_list_row_background, drawableResCaptor.captured)
|
||||
} finally {
|
||||
unmockkStatic(AppCompatResources::class)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN TopTabDecoration is invoked for a View THEN set the appropriate background`() {
|
||||
val view: View = mockk(relaxed = true)
|
||||
val drawable: Drawable = mockk()
|
||||
val drawableResCaptor = slot<Int>()
|
||||
|
||||
try {
|
||||
mockkStatic(AppCompatResources::class)
|
||||
every { AppCompatResources.getDrawable(any(), capture(drawableResCaptor)) } returns drawable
|
||||
|
||||
RecentTabViewDecorator.TopTabDecoration(view)
|
||||
|
||||
verify { view.background = drawable }
|
||||
assertEquals(R.drawable.rounded_top_corners, drawableResCaptor.captured)
|
||||
} finally {
|
||||
unmockkStatic(AppCompatResources::class)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN MiddleTabDecoration is invoked for a View THEN set the appropriate background and layout params`() {
|
||||
val colorAttrCaptor = slot<Int>()
|
||||
val viewLayoutParams: ViewGroup.MarginLayoutParams = mockk(relaxed = true)
|
||||
|
||||
try {
|
||||
mockkStatic("mozilla.components.support.ktx.android.util.DisplayMetricsKt")
|
||||
mockkStatic("mozilla.components.support.ktx.android.content.ContextKt")
|
||||
val view: View = mockk(relaxed = true) {
|
||||
every { layoutParams } returns viewLayoutParams
|
||||
every { context.getColorFromAttr(capture(colorAttrCaptor)) } returns 42
|
||||
every { context.resources.displayMetrics } returns mockk(relaxed = true)
|
||||
}
|
||||
every { any<Int>().dpToPx(any()) } returns 43
|
||||
|
||||
RecentTabViewDecorator.MiddleTabDecoration(view)
|
||||
|
||||
verify { view.setBackgroundColor(42) }
|
||||
assertEquals(R.attr.above, colorAttrCaptor.captured)
|
||||
assertEquals(43, viewLayoutParams.topMargin)
|
||||
} finally {
|
||||
unmockkStatic("mozilla.components.support.ktx.android.content.ContextKt")
|
||||
unmockkStatic("mozilla.components.support.ktx.android.util.DisplayMetricsKt")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN BottomTabDecoration is invoked for a View THEN set the appropriate background and layout params`() {
|
||||
val viewLayoutParams: ViewGroup.MarginLayoutParams = mockk(relaxed = true)
|
||||
val drawable: Drawable = mockk()
|
||||
val drawableResCaptor = slot<Int>()
|
||||
|
||||
try {
|
||||
mockkStatic(AppCompatResources::class)
|
||||
every { AppCompatResources.getDrawable(any(), capture(drawableResCaptor)) } returns drawable
|
||||
mockkStatic("mozilla.components.support.ktx.android.util.DisplayMetricsKt")
|
||||
val view: View = mockk(relaxed = true) {
|
||||
every { layoutParams } returns viewLayoutParams
|
||||
every { context.resources.displayMetrics } returns mockk(relaxed = true)
|
||||
}
|
||||
every { any<Int>().dpToPx(any()) } returns 43
|
||||
|
||||
RecentTabViewDecorator.BottomTabDecoration(view)
|
||||
|
||||
verify { view.background = drawable }
|
||||
assertEquals(R.drawable.rounded_bottom_corners, drawableResCaptor.captured)
|
||||
assertEquals(43, viewLayoutParams.topMargin)
|
||||
} finally {
|
||||
unmockkStatic("mozilla.components.support.ktx.android.util.DisplayMetricsKt")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
/* 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.sessioncontrol
|
||||
|
||||
import io.mockk.mockk
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.home.recenttabs.view.RecentTabsItemPosition
|
||||
|
||||
class SessionControlViewTest {
|
||||
@Test
|
||||
fun `GIVEN two recent tabs WHEN showRecentTabs is called THEN add the header, and two recent items to be shown`() {
|
||||
val recentTab: TabSessionState = mockk()
|
||||
val mediaTab: TabSessionState = mockk()
|
||||
val items = mutableListOf<AdapterItem>()
|
||||
|
||||
showRecentTabs(listOf(recentTab, mediaTab), items)
|
||||
|
||||
assertEquals(3, items.size)
|
||||
assertTrue(items[0] is AdapterItem.RecentTabsHeader)
|
||||
assertEquals(recentTab, (items[1] as AdapterItem.RecentTabItem).tab)
|
||||
assertEquals(mediaTab, (items[2] as AdapterItem.RecentTabItem).tab)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN one recent tab WHEN showRecentTabs is called THEN add the header and the recent tab to items shown`() {
|
||||
val recentTab: TabSessionState = mockk()
|
||||
val items = mutableListOf<AdapterItem>()
|
||||
|
||||
showRecentTabs(listOf(recentTab), items)
|
||||
|
||||
assertEquals(2, items.size)
|
||||
assertTrue(items[0] is AdapterItem.RecentTabsHeader)
|
||||
assertEquals(recentTab, (items[1] as AdapterItem.RecentTabItem).tab)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN only one recent tab and no media tab WHEN showRecentTabs is called THEN add the recent item as a single one to be shown`() {
|
||||
val recentTab: TabSessionState = mockk()
|
||||
val items = mutableListOf<AdapterItem>()
|
||||
|
||||
showRecentTabs(listOf(recentTab), items)
|
||||
|
||||
assertEquals(recentTab, (items[1] as AdapterItem.RecentTabItem).tab)
|
||||
assertSame(RecentTabsItemPosition.SINGLE, (items[1] as AdapterItem.RecentTabItem).position)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN two recent tabs WHEN showRecentTabs is called THEN add one item as top and one as bottom to be shown`() {
|
||||
val recentTab: TabSessionState = mockk()
|
||||
val mediaTab: TabSessionState = mockk()
|
||||
val items = mutableListOf<AdapterItem>()
|
||||
|
||||
showRecentTabs(listOf(recentTab, mediaTab), items)
|
||||
|
||||
assertEquals(recentTab, (items[1] as AdapterItem.RecentTabItem).tab)
|
||||
assertSame(RecentTabsItemPosition.TOP, (items[1] as AdapterItem.RecentTabItem).position)
|
||||
assertEquals(mediaTab, (items[2] as AdapterItem.RecentTabItem).tab)
|
||||
assertSame(RecentTabsItemPosition.BOTTOM, (items[2] as AdapterItem.RecentTabItem).position)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN three recent tabs WHEN showRecentTabs is called THEN add one recent item as top, one as middle and one as bottom to be shown`() {
|
||||
val recentTab1: TabSessionState = mockk()
|
||||
val recentTab2: TabSessionState = mockk()
|
||||
val mediaTab: TabSessionState = mockk()
|
||||
val items = mutableListOf<AdapterItem>()
|
||||
|
||||
showRecentTabs(listOf(recentTab1, recentTab2, mediaTab), items)
|
||||
|
||||
assertEquals(recentTab1, (items[1] as AdapterItem.RecentTabItem).tab)
|
||||
assertSame(RecentTabsItemPosition.TOP, (items[1] as AdapterItem.RecentTabItem).position)
|
||||
assertEquals(recentTab2, (items[2] as AdapterItem.RecentTabItem).tab)
|
||||
assertSame(RecentTabsItemPosition.MIDDLE, (items[2] as AdapterItem.RecentTabItem).position)
|
||||
assertEquals(mediaTab, (items[3] as AdapterItem.RecentTabItem).tab)
|
||||
assertSame(RecentTabsItemPosition.BOTTOM, (items[3] as AdapterItem.RecentTabItem).position)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user