mirror of
https://github.com/fork-maintainers/iceraven-browser
synced 2024-11-19 09:25:34 +00:00
[fenix] Issue FNX-22435: Introduce History metadata
Co-authored-by: Grisha Kruglov <gkruglov@mozilla.com>
This commit is contained in:
parent
5159f28403
commit
0913ebfe40
@ -38,4 +38,9 @@ object FeatureFlags {
|
||||
* Enables the "recent" tabs feature in the home screen.
|
||||
*/
|
||||
val showRecentTabsFeature = Config.channel.isNightlyOrDebug
|
||||
|
||||
/**
|
||||
* Enables recording of history metadata.
|
||||
*/
|
||||
val historyMetadataFeature = Config.channel.isDebug
|
||||
}
|
||||
|
@ -206,9 +206,20 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
updateLastBrowseActivity()
|
||||
if (FeatureFlags.historyMetadataFeature) {
|
||||
updateHistoryMetadata()
|
||||
}
|
||||
pwaOnboardingObserver?.stop()
|
||||
}
|
||||
|
||||
private fun updateHistoryMetadata() {
|
||||
getCurrentTab()?.let { tab ->
|
||||
(tab as? TabSessionState)?.historyMetadata?.let {
|
||||
requireComponents.core.historyMetadataService.updateMetadata(it, tab)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeToTabCollections() {
|
||||
Observer<List<TabCollection>> {
|
||||
requireComponents.core.tabCollectionStorage.cachedTabCollections = it
|
||||
|
@ -63,12 +63,16 @@ import mozilla.components.support.locale.LocaleManager
|
||||
import org.mozilla.fenix.AppRequestInterceptor
|
||||
import org.mozilla.fenix.BuildConfig
|
||||
import org.mozilla.fenix.Config
|
||||
import org.mozilla.fenix.FeatureFlags
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.search.SearchMigration
|
||||
import org.mozilla.fenix.downloads.DownloadService
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.historymetadata.DefaultHistoryMetadataService
|
||||
import org.mozilla.fenix.historymetadata.HistoryMetadataMiddleware
|
||||
import org.mozilla.fenix.historymetadata.HistoryMetadataService
|
||||
import org.mozilla.fenix.media.MediaSessionService
|
||||
import org.mozilla.fenix.perf.StrictModeManager
|
||||
import org.mozilla.fenix.perf.lazyMonitored
|
||||
@ -206,6 +210,10 @@ class Core(
|
||||
PromptMiddleware()
|
||||
)
|
||||
|
||||
if (FeatureFlags.historyMetadataFeature) {
|
||||
middlewareList += HistoryMetadataMiddleware(historyMetadataService)
|
||||
}
|
||||
|
||||
BrowserStore(
|
||||
middleware = middlewareList + EngineMiddleware.create(engine)
|
||||
).apply {
|
||||
@ -239,6 +247,15 @@ class Core(
|
||||
StatementRelationChecker(StatementApi(client))
|
||||
}
|
||||
|
||||
/**
|
||||
* The [HistoryMetadataService] is used to record history metadata.
|
||||
*/
|
||||
val historyMetadataService: HistoryMetadataService by lazyMonitored {
|
||||
DefaultHistoryMetadataService(storage = historyStorage).apply {
|
||||
cleanup(System.currentTimeMillis() - HISTORY_METADATA_MAX_AGE_IN_MS)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Icons component for loading, caching and processing website icons.
|
||||
*/
|
||||
@ -424,5 +441,6 @@ class Core(
|
||||
private const val KEY_STORAGE_NAME = "core_prefs"
|
||||
private const val PASSWORDS_KEY = "passwords"
|
||||
private const val RECENTLY_CLOSED_MAX = 10
|
||||
private const val HISTORY_METADATA_MAX_AGE_IN_MS = 7 * 24 * 60 * 60 * 1000 // 7 days
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,128 @@
|
||||
/* 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.historymetadata
|
||||
|
||||
import mozilla.components.browser.state.action.BrowserAction
|
||||
import mozilla.components.browser.state.action.ContentAction
|
||||
import mozilla.components.browser.state.action.HistoryMetadataAction
|
||||
import mozilla.components.browser.state.action.MediaSessionAction
|
||||
import mozilla.components.browser.state.action.TabListAction
|
||||
import mozilla.components.browser.state.selector.findTab
|
||||
import mozilla.components.browser.state.selector.normalTabs
|
||||
import mozilla.components.browser.state.selector.selectedTab
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import mozilla.components.lib.state.Middleware
|
||||
import mozilla.components.lib.state.MiddlewareContext
|
||||
import mozilla.components.lib.state.Store
|
||||
|
||||
/**
|
||||
* This [Middleware] reacts to various browsing events and records history metadata as needed.
|
||||
*/
|
||||
class HistoryMetadataMiddleware(
|
||||
private val historyMetadataService: HistoryMetadataService
|
||||
) : Middleware<BrowserState, BrowserAction> {
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
override fun invoke(
|
||||
context: MiddlewareContext<BrowserState, BrowserAction>,
|
||||
next: (BrowserAction) -> Unit,
|
||||
action: BrowserAction
|
||||
) {
|
||||
// Pre process actions
|
||||
when (action) {
|
||||
is TabListAction.AddTabAction -> {
|
||||
if (action.select) {
|
||||
// Before we add and select a new tab we update the metadata
|
||||
// of the currently selected tab, if not private.
|
||||
context.state.selectedNormalTab?.let {
|
||||
updateHistoryMetadata(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
is TabListAction.SelectTabAction -> {
|
||||
// Before we select a new tab we update the metadata
|
||||
// of the currently selected tab, if not private.
|
||||
context.state.selectedNormalTab?.let {
|
||||
updateHistoryMetadata(it)
|
||||
}
|
||||
}
|
||||
is TabListAction.RemoveTabAction -> {
|
||||
if (action.tabId == context.state.selectedTabId) {
|
||||
context.state.findNormalTab(action.tabId)?.let {
|
||||
updateHistoryMetadata(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
is TabListAction.RemoveTabsAction -> {
|
||||
action.tabIds.find { it == context.state.selectedTabId }?.let {
|
||||
context.state.findNormalTab(it)?.let { tab ->
|
||||
updateHistoryMetadata(tab)
|
||||
}
|
||||
}
|
||||
}
|
||||
is ContentAction.UpdateLoadingStateAction -> {
|
||||
context.state.findNormalTab(action.sessionId)?.let { tab ->
|
||||
val selectedTab = tab.id == context.state.selectedTabId
|
||||
if (tab.content.loading && !action.loading) {
|
||||
// When a page stops loading we record its metadata
|
||||
createHistoryMetadata(context, tab)
|
||||
} else if (!tab.content.loading && action.loading && selectedTab) {
|
||||
// When a page starts loading (e.g. user navigated away by
|
||||
// clicking on a link) we update metadata
|
||||
updateHistoryMetadata(tab)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
next(action)
|
||||
|
||||
// Post process actions
|
||||
when (action) {
|
||||
// We're handling this after processing the action because we want the tab
|
||||
// state to contain the updated media session state.
|
||||
is MediaSessionAction.UpdateMediaMetadataAction -> {
|
||||
context.state.findNormalTab(action.tabId)?.let { tab ->
|
||||
createHistoryMetadata(context, tab)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createHistoryMetadata(context: MiddlewareContext<BrowserState, BrowserAction>, tab: TabSessionState) {
|
||||
val key = historyMetadataService.createMetadata(tab, tab.getParent(context.store))
|
||||
context.dispatch(HistoryMetadataAction.SetHistoryMetadataKeyAction(tab.id, key))
|
||||
}
|
||||
|
||||
private fun updateHistoryMetadata(tab: TabSessionState) {
|
||||
tab.historyMetadata?.let {
|
||||
historyMetadataService.updateMetadata(it, tab)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TabSessionState.getParent(store: Store<BrowserState, BrowserAction>): TabSessionState? {
|
||||
return parentId?.let {
|
||||
store.state.findTab(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns the normal (non-private) tab with the given id. Returns null if no
|
||||
* matching tab could be found.
|
||||
*
|
||||
* @param tabId The ID of the tab to search for.
|
||||
* @return The [TabSessionState] with the provided [tabId] or null if it could not be found.
|
||||
*/
|
||||
private fun BrowserState.findNormalTab(tabId: String): TabSessionState? {
|
||||
return normalTabs.firstOrNull { it.id == tabId }
|
||||
}
|
||||
|
||||
/**
|
||||
* The currently selected tab if there's one that is not private.
|
||||
*/
|
||||
private val BrowserState.selectedNormalTab: TabSessionState?
|
||||
get() = selectedTabId?.let { id -> findNormalTab(id) }
|
@ -0,0 +1,103 @@
|
||||
/* 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.historymetadata
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import mozilla.components.concept.storage.DocumentType
|
||||
import mozilla.components.concept.storage.HistoryMetadataKey
|
||||
import mozilla.components.concept.storage.HistoryMetadataObservation
|
||||
import mozilla.components.concept.storage.HistoryMetadataStorage
|
||||
import mozilla.components.support.base.log.logger.Logger
|
||||
|
||||
/**
|
||||
* Service for managing (creating, updating, deleting) history metadata.
|
||||
*/
|
||||
interface HistoryMetadataService {
|
||||
|
||||
/**
|
||||
* Creates a history metadata record for the provided tab.
|
||||
*
|
||||
* @param tab the [TabSessionState] to record metadata for.
|
||||
* @param parent the parent [TabSessionState] for search and domain grouping purposes.
|
||||
*/
|
||||
fun createMetadata(tab: TabSessionState, parent: TabSessionState? = null): HistoryMetadataKey
|
||||
|
||||
/**
|
||||
* Updates the history metadata corresponding to the provided tab.
|
||||
*
|
||||
* @param key the [HistoryMetadataKey] identifying history metadata.
|
||||
* @param tab the [TabSessionState] to update history metadata for.
|
||||
*/
|
||||
fun updateMetadata(key: HistoryMetadataKey, tab: TabSessionState)
|
||||
|
||||
/**
|
||||
* Deletes history metadata records that haven't been updated since
|
||||
* the specified timestamp.
|
||||
*
|
||||
* @param olderThan timestamp indicating which records to delete.
|
||||
*/
|
||||
fun cleanup(olderThan: Long)
|
||||
}
|
||||
|
||||
class DefaultHistoryMetadataService(
|
||||
private val storage: HistoryMetadataStorage,
|
||||
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO)
|
||||
) : HistoryMetadataService {
|
||||
|
||||
private val logger = Logger("DefaultHistoryMetadataService")
|
||||
|
||||
override fun createMetadata(tab: TabSessionState, parent: TabSessionState?): HistoryMetadataKey {
|
||||
logger.debug("Creating metadata for tab ${tab.id}")
|
||||
|
||||
val existingMetadata = tab.historyMetadata
|
||||
val metadataKey = if (existingMetadata != null && existingMetadata.url == tab.content.url) {
|
||||
existingMetadata
|
||||
} else {
|
||||
tab.toHistoryMetadataKey(parent)
|
||||
}
|
||||
|
||||
val documentTypeObservation = HistoryMetadataObservation.DocumentTypeObservation(
|
||||
documentType = when (tab.mediaSessionState) {
|
||||
null -> DocumentType.Regular
|
||||
else -> DocumentType.Media
|
||||
}
|
||||
)
|
||||
|
||||
scope.launch {
|
||||
storage.noteHistoryMetadataObservation(metadataKey, documentTypeObservation)
|
||||
}
|
||||
|
||||
return metadataKey
|
||||
}
|
||||
|
||||
override fun updateMetadata(key: HistoryMetadataKey, tab: TabSessionState) {
|
||||
logger.debug("Updating metadata for tab $tab")
|
||||
|
||||
scope.launch {
|
||||
val viewTimeObservation = HistoryMetadataObservation.ViewTimeObservation(
|
||||
viewTime = (System.currentTimeMillis() - tab.lastAccess).toInt()
|
||||
)
|
||||
storage.noteHistoryMetadataObservation(key, viewTimeObservation)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cleanup(olderThan: Long) {
|
||||
logger.debug("Deleting metadata last updated before $olderThan")
|
||||
|
||||
scope.launch {
|
||||
storage.deleteHistoryMetadataOlderThan(olderThan)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun TabSessionState.toHistoryMetadataKey(parent: TabSessionState? = null): HistoryMetadataKey =
|
||||
HistoryMetadataKey(
|
||||
url = content.url,
|
||||
referrerUrl = parent?.content?.url,
|
||||
searchTerm = parent?.content?.searchTerms.takeUnless { it.isNullOrEmpty() }
|
||||
)
|
@ -0,0 +1,302 @@
|
||||
/* 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.historymetadata
|
||||
|
||||
import io.mockk.Called
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import mozilla.components.browser.state.action.ContentAction
|
||||
import mozilla.components.browser.state.action.MediaSessionAction
|
||||
import mozilla.components.browser.state.action.TabListAction
|
||||
import mozilla.components.browser.state.engine.EngineMiddleware
|
||||
import mozilla.components.browser.state.selector.findTab
|
||||
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.concept.storage.HistoryMetadataKey
|
||||
import mozilla.components.support.test.ext.joinBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
class HistoryMetadataMiddlewareTest {
|
||||
|
||||
private lateinit var store: BrowserStore
|
||||
private lateinit var middleware: HistoryMetadataMiddleware
|
||||
private lateinit var service: HistoryMetadataService
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
service = mockk(relaxed = true)
|
||||
middleware = HistoryMetadataMiddleware(service)
|
||||
store = BrowserStore(
|
||||
middleware = listOf(middleware) + EngineMiddleware.create(engine = mockk()),
|
||||
initialState = BrowserState()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN normal tab WHEN loading completed THEN meta data is recorded`() {
|
||||
val tab = createTab("https://mozilla.org")
|
||||
|
||||
val expectedKey = HistoryMetadataKey(url = tab.content.url)
|
||||
every { service.createMetadata(any(), any()) } returns expectedKey
|
||||
|
||||
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
|
||||
store.dispatch(ContentAction.UpdateLoadingStateAction(tab.id, true)).joinBlocking()
|
||||
verify { service wasNot Called }
|
||||
|
||||
store.dispatch(ContentAction.UpdateLoadingStateAction(tab.id, false)).joinBlocking()
|
||||
val capturedTab = slot<TabSessionState>()
|
||||
verify { service.createMetadata(capture(capturedTab), null) }
|
||||
|
||||
assertEquals(tab.id, capturedTab.captured.id)
|
||||
assertEquals(expectedKey, store.state.findTab(tab.id)?.historyMetadata)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN private tab WHEN loading completed THEN no meta data is recorded`() {
|
||||
val tab = createTab("https://mozilla.org", private = true)
|
||||
|
||||
val expectedKey = HistoryMetadataKey(url = tab.content.url)
|
||||
every { service.createMetadata(any(), any()) } returns expectedKey
|
||||
|
||||
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
|
||||
store.dispatch(ContentAction.UpdateLoadingStateAction(tab.id, true)).joinBlocking()
|
||||
verify { service wasNot Called }
|
||||
|
||||
store.dispatch(ContentAction.UpdateLoadingStateAction(tab.id, false)).joinBlocking()
|
||||
verify { service wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN normal tab WHEN user navigates and new page starts loading THEN meta data is updated`() {
|
||||
val existingKey = HistoryMetadataKey(url = "https://mozilla.org")
|
||||
val tab = createTab(url = existingKey.url, historyMetadata = existingKey)
|
||||
|
||||
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
|
||||
verify { service wasNot Called }
|
||||
|
||||
store.dispatch(ContentAction.UpdateLoadingStateAction(tab.id, true)).joinBlocking()
|
||||
val capturedTab = slot<TabSessionState>()
|
||||
verify { service.updateMetadata(existingKey, capture(capturedTab)) }
|
||||
|
||||
assertEquals(tab.id, capturedTab.captured.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tab without meta data WHEN user navigates and new page starts loading THEN nothing happens`() {
|
||||
val tab = createTab(url = "https://mozilla.org")
|
||||
|
||||
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
|
||||
verify { service wasNot Called }
|
||||
|
||||
store.dispatch(ContentAction.UpdateLoadingStateAction(tab.id, true)).joinBlocking()
|
||||
verify { service wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tab is not selected WHEN user navigates and new page starts loading THEN nothing happens`() {
|
||||
val existingKey = HistoryMetadataKey(url = "https://mozilla.org")
|
||||
val tab = createTab(url = "https://mozilla.org", historyMetadata = existingKey)
|
||||
val otherTab = createTab(url = "https://blog.mozilla.org")
|
||||
|
||||
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
|
||||
store.dispatch(TabListAction.AddTabAction(otherTab, select = true)).joinBlocking()
|
||||
val capturedTab = slot<TabSessionState>()
|
||||
verify(exactly = 1) { service.updateMetadata(existingKey, capture(capturedTab)) }
|
||||
assertEquals(tab.id, capturedTab.captured.id)
|
||||
|
||||
store.dispatch(ContentAction.UpdateLoadingStateAction(tab.id, true)).joinBlocking()
|
||||
verify(exactly = 1) { service.updateMetadata(existingKey, capture(capturedTab)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN normal media tab WHEN media state is updated THEN meta data is recorded`() {
|
||||
val tab = createTab("https://media.mozilla.org")
|
||||
|
||||
val expectedKey = HistoryMetadataKey(url = tab.content.url)
|
||||
every { service.createMetadata(any(), any()) } returns expectedKey
|
||||
|
||||
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
|
||||
verify { service wasNot Called }
|
||||
|
||||
store.dispatch(MediaSessionAction.UpdateMediaMetadataAction(tab.id, mockk())).joinBlocking()
|
||||
val capturedTab = slot<TabSessionState>()
|
||||
verify { service.createMetadata(capture(capturedTab), null) }
|
||||
|
||||
assertEquals(tab.id, capturedTab.captured.id)
|
||||
assertEquals(expectedKey, store.state.findTab(tab.id)?.historyMetadata)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN private media tab WHEN media state is updated THEN no meta data is recorded`() {
|
||||
val tab = createTab("https://media.mozilla.org", private = true)
|
||||
|
||||
val expectedKey = HistoryMetadataKey(url = tab.content.url)
|
||||
every { service.createMetadata(any(), any()) } returns expectedKey
|
||||
|
||||
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
|
||||
verify { service wasNot Called }
|
||||
|
||||
store.dispatch(MediaSessionAction.UpdateMediaMetadataAction(tab.id, mockk())).joinBlocking()
|
||||
verify { service wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN normal tab is selected WHEN new tab will be added and selected THEN meta data is updated for currently selected tab`() {
|
||||
val existingKey = HistoryMetadataKey(url = "https://mozilla.org")
|
||||
val tab = createTab(url = "https://mozilla.org", historyMetadata = existingKey)
|
||||
val otherTab = createTab(url = "https://blog.mozilla.org")
|
||||
val yetAnotherTab = createTab(url = "https://media.mozilla.org")
|
||||
|
||||
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
|
||||
store.dispatch(TabListAction.AddTabAction(otherTab)).joinBlocking()
|
||||
verify { service wasNot Called }
|
||||
|
||||
store.dispatch(TabListAction.AddTabAction(yetAnotherTab, select = true)).joinBlocking()
|
||||
val capturedTab = slot<TabSessionState>()
|
||||
verify(exactly = 1) { service.updateMetadata(existingKey, capture(capturedTab)) }
|
||||
assertEquals(tab.id, capturedTab.captured.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN private tab is selected WHEN new tab will be added and selected THEN nothing happens`() {
|
||||
val existingKey = HistoryMetadataKey(url = "https://mozilla.org")
|
||||
val tab = createTab(url = "https://mozilla.org", historyMetadata = existingKey, private = true)
|
||||
val otherTab = createTab(url = "https://blog.mozilla.org")
|
||||
val yetAnotherTab = createTab(url = "https://media.mozilla.org")
|
||||
|
||||
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
|
||||
store.dispatch(TabListAction.AddTabAction(otherTab)).joinBlocking()
|
||||
verify { service wasNot Called }
|
||||
|
||||
store.dispatch(TabListAction.AddTabAction(yetAnotherTab, select = true)).joinBlocking()
|
||||
verify { service wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN normal tab is selected WHEN new tab will be selected THEN meta data is updated for currently selected tab`() {
|
||||
val existingKey = HistoryMetadataKey(url = "https://mozilla.org")
|
||||
val tab = createTab(url = "https://mozilla.org", historyMetadata = existingKey)
|
||||
val otherTab = createTab(url = "https://blog.mozilla.org")
|
||||
|
||||
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
|
||||
verify { service wasNot Called }
|
||||
|
||||
store.dispatch(TabListAction.SelectTabAction(otherTab.id)).joinBlocking()
|
||||
val capturedTab = slot<TabSessionState>()
|
||||
verify(exactly = 1) { service.updateMetadata(existingKey, capture(capturedTab)) }
|
||||
assertEquals(tab.id, capturedTab.captured.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN private tab is selected WHEN new tab will be selected THEN nothing happens`() {
|
||||
val existingKey = HistoryMetadataKey(url = "https://mozilla.org")
|
||||
val tab = createTab(url = "https://mozilla.org", historyMetadata = existingKey, private = true)
|
||||
val otherTab = createTab(url = "https://blog.mozilla.org")
|
||||
|
||||
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
|
||||
verify { service wasNot Called }
|
||||
|
||||
store.dispatch(TabListAction.SelectTabAction(otherTab.id)).joinBlocking()
|
||||
verify { service wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN normal selected tab is removed THEN meta data is updated`() {
|
||||
val existingKey = HistoryMetadataKey(url = "https://mozilla.org")
|
||||
val tab = createTab(url = "https://mozilla.org", historyMetadata = existingKey)
|
||||
|
||||
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
|
||||
verify { service wasNot Called }
|
||||
|
||||
store.dispatch(TabListAction.RemoveTabAction(tab.id)).joinBlocking()
|
||||
val capturedTab = slot<TabSessionState>()
|
||||
verify(exactly = 1) { service.updateMetadata(existingKey, capture(capturedTab)) }
|
||||
assertEquals(tab.id, capturedTab.captured.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN private selected tab is removed THEN nothing happens`() {
|
||||
val existingKey = HistoryMetadataKey(url = "https://mozilla.org")
|
||||
val tab = createTab(url = "https://mozilla.org", historyMetadata = existingKey, private = true)
|
||||
|
||||
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
|
||||
verify { service wasNot Called }
|
||||
|
||||
store.dispatch(TabListAction.RemoveTabAction(tab.id)).joinBlocking()
|
||||
verify { service wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN non-selected tab is removed THEN nothing happens`() {
|
||||
val existingKey = HistoryMetadataKey(url = "https://mozilla.org")
|
||||
val tab = createTab(url = "https://mozilla.org", historyMetadata = existingKey)
|
||||
val otherTab = createTab(url = "https://blog.mozilla.org")
|
||||
|
||||
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
|
||||
store.dispatch(TabListAction.AddTabAction(otherTab)).joinBlocking()
|
||||
verify { service wasNot Called }
|
||||
|
||||
store.dispatch(TabListAction.RemoveTabAction(otherTab.id)).joinBlocking()
|
||||
verify { service wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN multiple tabs are removed WHEN selected normal tab should also be removed THEN meta data is updated`() {
|
||||
val existingKey = HistoryMetadataKey(url = "https://mozilla.org")
|
||||
val tab = createTab(url = "https://mozilla.org", historyMetadata = existingKey)
|
||||
val otherTab = createTab(url = "https://blog.mozilla.org")
|
||||
val yetAnotherTab = createTab(url = "https://media.mozilla.org")
|
||||
|
||||
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
|
||||
store.dispatch(TabListAction.AddTabAction(otherTab)).joinBlocking()
|
||||
store.dispatch(TabListAction.AddTabAction(yetAnotherTab)).joinBlocking()
|
||||
verify { service wasNot Called }
|
||||
|
||||
store.dispatch(TabListAction.RemoveTabsAction(listOf(tab.id, otherTab.id))).joinBlocking()
|
||||
val capturedTab = slot<TabSessionState>()
|
||||
verify(exactly = 1) { service.updateMetadata(existingKey, capture(capturedTab)) }
|
||||
assertEquals(tab.id, capturedTab.captured.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN multiple tabs are removed WHEN selected private tab should also be removed THEN nothing happens`() {
|
||||
val existingKey = HistoryMetadataKey(url = "https://mozilla.org")
|
||||
val tab = createTab(url = "https://mozilla.org", historyMetadata = existingKey, private = true)
|
||||
val otherTab = createTab(url = "https://blog.mozilla.org")
|
||||
val yetAnotherTab = createTab(url = "https://media.mozilla.org")
|
||||
|
||||
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
|
||||
store.dispatch(TabListAction.AddTabAction(otherTab)).joinBlocking()
|
||||
store.dispatch(TabListAction.AddTabAction(yetAnotherTab)).joinBlocking()
|
||||
verify { service wasNot Called }
|
||||
|
||||
store.dispatch(TabListAction.RemoveTabsAction(listOf(tab.id, otherTab.id))).joinBlocking()
|
||||
verify { service wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN multiple tabs are removed WHEN selected tab should not be removed THEN nothing happens`() {
|
||||
val existingKey = HistoryMetadataKey(url = "https://mozilla.org")
|
||||
val tab = createTab(url = "https://mozilla.org", historyMetadata = existingKey)
|
||||
val otherTab = createTab(url = "https://blog.mozilla.org")
|
||||
val yetAnotherTab = createTab(url = "https://media.mozilla.org")
|
||||
|
||||
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
|
||||
store.dispatch(TabListAction.AddTabAction(otherTab)).joinBlocking()
|
||||
store.dispatch(TabListAction.AddTabAction(yetAnotherTab)).joinBlocking()
|
||||
verify { service wasNot Called }
|
||||
|
||||
store.dispatch(TabListAction.RemoveTabsAction(listOf(otherTab.id, yetAnotherTab.id))).joinBlocking()
|
||||
verify { service wasNot Called }
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
/* 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.historymetadata
|
||||
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestCoroutineDispatcher
|
||||
import mozilla.components.browser.state.state.createTab
|
||||
import mozilla.components.concept.storage.DocumentType
|
||||
import mozilla.components.concept.storage.HistoryMetadataKey
|
||||
import mozilla.components.concept.storage.HistoryMetadataObservation
|
||||
import mozilla.components.concept.storage.HistoryMetadataStorage
|
||||
import mozilla.components.support.test.rule.MainCoroutineRule
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
class HistoryMetadataServiceTest {
|
||||
|
||||
private lateinit var service: HistoryMetadataService
|
||||
private lateinit var storage: HistoryMetadataStorage
|
||||
|
||||
val testDispatcher = TestCoroutineDispatcher()
|
||||
|
||||
@get:Rule
|
||||
val coroutinesTestRule = MainCoroutineRule(testDispatcher)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
storage = mockk(relaxed = true)
|
||||
service = DefaultHistoryMetadataService(storage, CoroutineScope(testDispatcher))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a regular page WHEN metadata is created THEN a regular document type observation is recorded`() {
|
||||
val parent = createTab("https://mozilla.org")
|
||||
val tab = createTab("https://blog.mozilla.org", parent = parent)
|
||||
service.createMetadata(tab, parent)
|
||||
testDispatcher.advanceUntilIdle()
|
||||
|
||||
val expectedKey = HistoryMetadataKey(url = tab.content.url, referrerUrl = parent.content.url)
|
||||
val expectedObservation = HistoryMetadataObservation.DocumentTypeObservation(documentType = DocumentType.Regular)
|
||||
coVerify { storage.noteHistoryMetadataObservation(expectedKey, expectedObservation) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a media page WHEN metadata is created THEN a media document type observation is recorded`() {
|
||||
val tab = createTab("https://media.mozilla.org", mediaSessionState = mockk())
|
||||
service.createMetadata(tab)
|
||||
testDispatcher.advanceUntilIdle()
|
||||
|
||||
val expectedKey = HistoryMetadataKey(url = tab.content.url)
|
||||
val expectedObservation = HistoryMetadataObservation.DocumentTypeObservation(documentType = DocumentType.Media)
|
||||
coVerify { storage.noteHistoryMetadataObservation(expectedKey, expectedObservation) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN existing metadata WHEN metadata is created THEN correct document type observation is recorded`() {
|
||||
val existingKey = HistoryMetadataKey(url = "https://media.mozilla.org", referrerUrl = "https://mozilla.org")
|
||||
val tab = createTab("https://media.mozilla.org", historyMetadata = existingKey)
|
||||
service.createMetadata(tab)
|
||||
testDispatcher.advanceUntilIdle()
|
||||
|
||||
var expectedKey = HistoryMetadataKey(url = tab.content.url, referrerUrl = existingKey.referrerUrl)
|
||||
var expectedObservation = HistoryMetadataObservation.DocumentTypeObservation(documentType = DocumentType.Regular)
|
||||
coVerify { storage.noteHistoryMetadataObservation(expectedKey, expectedObservation) }
|
||||
|
||||
val otherTab = createTab("https://blog.mozilla.org", historyMetadata = existingKey)
|
||||
service.createMetadata(otherTab)
|
||||
testDispatcher.advanceUntilIdle()
|
||||
|
||||
expectedKey = HistoryMetadataKey(url = otherTab.content.url)
|
||||
expectedObservation = HistoryMetadataObservation.DocumentTypeObservation(documentType = DocumentType.Regular)
|
||||
coVerify { storage.noteHistoryMetadataObservation(expectedKey, expectedObservation) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN metadata is updated THEN a view time observation is recorded`() {
|
||||
val now = System.currentTimeMillis()
|
||||
val key = HistoryMetadataKey(url = "https://blog.mozilla.org")
|
||||
val tab = createTab(key.url, historyMetadata = key, lastAccess = now - 60 * 1000)
|
||||
service.updateMetadata(key, tab)
|
||||
testDispatcher.advanceUntilIdle()
|
||||
|
||||
val observation = slot<HistoryMetadataObservation.ViewTimeObservation>()
|
||||
coVerify { storage.noteHistoryMetadataObservation(key, capture(observation)) }
|
||||
assertTrue(observation.captured.viewTime >= 60 * 1000)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN cleanup is called THEN old metadata is deleted`() {
|
||||
val timestamp = System.currentTimeMillis() - 7 * 24 * 60 * 60 * 1000
|
||||
service.cleanup(timestamp)
|
||||
testDispatcher.advanceUntilIdle()
|
||||
|
||||
coVerify { storage.deleteHistoryMetadataOlderThan(timestamp) }
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user