For #19933 - Show a media tab item on homescreen for the last tab with media

This commit is contained in:
Mugurell 2021-06-23 20:14:55 +03:00
parent 68b56ff240
commit 1251894933
12 changed files with 673 additions and 72 deletions

View File

@ -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) {

View File

@ -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 if (tab != null && !tab.content.private) {
listOfNotNull(tab)
} else {
emptyList()
return mutableListOf<TabSessionState>().apply {
val lastOpenedNormalTab = lastOpenedNormalTab
lastOpenedNormalTab?.let { add(it) }
inProgressMediaTab
?.takeUnless { it == lastOpenedNormalTab }
?.let {
add(it)
}
}
}
/**
* 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 }

View File

@ -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()
}
homeStore.dispatch(HomeFragmentAction.RecentTabsChange(recentTabsList))
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
)
}
.collect {
homeStore.dispatch(HomeFragmentAction.RecentTabsChange(browserStore.state.asRecentTabs()))
}
}
}

View File

@ -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
}
}
}

View File

@ -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 {

View File

@ -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(

View File

@ -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))
}
}
}

View File

@ -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">

View File

@ -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)
}
}

View File

@ -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])
}
}

View File

@ -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")
}
}
}

View File

@ -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)
}
}