Issue FNX-22435: Introduce History metadata

Co-authored-by: Grisha Kruglov <gkruglov@mozilla.com>
upstream-sync
Christian Sadilek 3 years ago
parent 7959b427c8
commit ba19960b7e

@ -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…
Cancel
Save