Merge remote-tracking branch 'upstream/master' into fork
commit
e8c354f0bb
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,17 @@
|
||||
/* 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.search.ext
|
||||
|
||||
import android.content.Context
|
||||
import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider
|
||||
|
||||
private const val MINIMUM_SEARCH_ENGINES_NUMBER_TO_SHOW_SHORTCUTS = 2
|
||||
|
||||
/**
|
||||
* Return if the user has *at least 2* installed search engines.
|
||||
* Useful to decide whether to show / enable certain functionalities.
|
||||
*/
|
||||
fun FenixSearchEngineProvider.areShortcutsAvailable(context: Context) =
|
||||
installedSearchEngines(context).list.size >= MINIMUM_SEARCH_ENGINES_NUMBER_TO_SHOW_SHORTCUTS
|
@ -1,51 +0,0 @@
|
||||
/* 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.session
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.map
|
||||
import mozilla.components.browser.state.selector.privateTabs
|
||||
import mozilla.components.lib.state.ext.flowScoped
|
||||
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
|
||||
import org.mozilla.fenix.ext.components
|
||||
|
||||
/**
|
||||
* This observer starts and stops the service to show a notification
|
||||
* indicating that a private tab is open.
|
||||
*/
|
||||
class NotificationSessionObserver(
|
||||
private val applicationContext: Context,
|
||||
private val notificationService: SessionNotificationService.Companion = SessionNotificationService
|
||||
) {
|
||||
|
||||
private var scope: CoroutineScope? = null
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
fun start() {
|
||||
scope = applicationContext.components.core.store.flowScoped { flow ->
|
||||
flow.map { state -> state.privateTabs.isNotEmpty() }
|
||||
.ifChanged()
|
||||
.collect { hasPrivateTabs ->
|
||||
if (hasPrivateTabs) {
|
||||
notificationService.start(applicationContext, isStartedFromPrivateShortcut)
|
||||
} else if (SessionNotificationService.started) {
|
||||
notificationService.stop(applicationContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
scope?.cancel()
|
||||
}
|
||||
|
||||
companion object {
|
||||
var isStartedFromPrivateShortcut = false
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
/* 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.session
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.feature.privatemode.notification.AbstractPrivateNotificationService
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.metrics
|
||||
|
||||
/**
|
||||
* Manages notifications for private tabs.
|
||||
*
|
||||
* Private tab notifications solve two problems for us:
|
||||
* 1 - They allow users to interact with us from outside of the app (example: by closing all
|
||||
* private tabs).
|
||||
* 2 - The notification will keep our process alive, allowing us to keep private tabs in memory.
|
||||
*
|
||||
* As long as a session is active this service will keep its notification alive.
|
||||
*/
|
||||
class PrivateNotificationService : AbstractPrivateNotificationService() {
|
||||
|
||||
override val store: BrowserStore by lazy { components.core.store }
|
||||
|
||||
override fun NotificationCompat.Builder.buildNotification() {
|
||||
setSmallIcon(R.drawable.ic_pbm_notification)
|
||||
setContentTitle(getString(R.string.app_name_private_4, getString(R.string.app_name)))
|
||||
setContentText(getString(R.string.notification_pbm_delete_text_2))
|
||||
color = ContextCompat.getColor(this@PrivateNotificationService, R.color.pbm_notification_color)
|
||||
}
|
||||
|
||||
override fun erasePrivateTabs() {
|
||||
metrics.track(Event.PrivateBrowsingNotificationTapped)
|
||||
|
||||
val homeScreenIntent = Intent(this, HomeActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
putExtra(HomeActivity.PRIVATE_BROWSING_MODE, isStartedFromPrivateShortcut)
|
||||
}
|
||||
|
||||
if (VisibilityLifecycleCallback.finishAndRemoveTaskIfInBackground(this)) {
|
||||
// Set start mode to be in background (recents screen)
|
||||
homeScreenIntent.apply {
|
||||
putExtra(HomeActivity.START_IN_RECENTS_SCREEN, true)
|
||||
}
|
||||
}
|
||||
|
||||
startActivity(homeScreenIntent)
|
||||
super.erasePrivateTabs()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Global used by [HomeActivity] to figure out if normal mode or private mode
|
||||
* should be used after closing all private tabs.
|
||||
*/
|
||||
var isStartedFromPrivateShortcut = false
|
||||
}
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
/* 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.session
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import mozilla.components.browser.session.SessionManager
|
||||
import mozilla.components.support.utils.ThreadUtils
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.metrics
|
||||
import org.mozilla.fenix.ext.sessionsOfType
|
||||
|
||||
/**
|
||||
* Manages notifications for private tabs.
|
||||
*
|
||||
* Private tab notifications solve two problems for us:
|
||||
* 1 - They allow users to interact with us from outside of the app (example: by closing all
|
||||
* private tabs).
|
||||
* 2 - The notification will keep our process alive, allowing us to keep private tabs in memory.
|
||||
*
|
||||
* As long as a session is active this service will keep its notification alive.
|
||||
*/
|
||||
class SessionNotificationService : Service() {
|
||||
|
||||
private var isStartedFromPrivateShortcut: Boolean = false
|
||||
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
val action = intent.action ?: return START_NOT_STICKY
|
||||
|
||||
when (action) {
|
||||
ACTION_START -> {
|
||||
isStartedFromPrivateShortcut = intent.getBooleanExtra(STARTED_FROM_PRIVATE_SHORTCUT, false)
|
||||
createNotificationChannelIfNeeded()
|
||||
startForeground(NOTIFICATION_ID, buildNotification())
|
||||
}
|
||||
|
||||
ACTION_ERASE -> {
|
||||
metrics.track(Event.PrivateBrowsingNotificationTapped)
|
||||
|
||||
val homeScreenIntent = Intent(this, HomeActivity::class.java)
|
||||
val intentFlags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
homeScreenIntent.apply {
|
||||
setFlags(intentFlags)
|
||||
putExtra(HomeActivity.PRIVATE_BROWSING_MODE, isStartedFromPrivateShortcut)
|
||||
}
|
||||
if (VisibilityLifecycleCallback.finishAndRemoveTaskIfInBackground(this)) {
|
||||
// Set start mode to be in background (recents screen)
|
||||
homeScreenIntent.apply {
|
||||
putExtra(HomeActivity.START_IN_RECENTS_SCREEN, true)
|
||||
}
|
||||
}
|
||||
startActivity(homeScreenIntent)
|
||||
components.core.sessionManager.removeAndCloseAllPrivateSessions()
|
||||
}
|
||||
|
||||
else -> throw IllegalStateException("Unknown intent: $intent")
|
||||
}
|
||||
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent) {
|
||||
components.core.sessionManager.removeAndCloseAllPrivateSessions()
|
||||
|
||||
stopForeground(true)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun buildNotification(): Notification {
|
||||
return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.ic_pbm_notification)
|
||||
.setContentTitle(getString(R.string.app_name_private_4, getString(R.string.app_name)))
|
||||
.setContentText(getString(R.string.notification_pbm_delete_text_2))
|
||||
.setContentIntent(createNotificationIntent())
|
||||
.setVisibility(NotificationCompat.VISIBILITY_SECRET)
|
||||
.setShowWhen(false)
|
||||
.setLocalOnly(true)
|
||||
.setColor(ContextCompat.getColor(this, R.color.pbm_notification_color))
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createNotificationIntent(): PendingIntent {
|
||||
val intent = Intent(this, SessionNotificationService::class.java)
|
||||
intent.action = ACTION_ERASE
|
||||
|
||||
return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_ONE_SHOT)
|
||||
}
|
||||
|
||||
private fun createNotificationChannelIfNeeded() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
// Notification channels are only available on Android O or higher.
|
||||
return
|
||||
}
|
||||
|
||||
val notificationManager = getSystemService<NotificationManager>() ?: return
|
||||
|
||||
val notificationChannelName = getString(R.string.notification_pbm_channel_name)
|
||||
|
||||
val channel = NotificationChannel(
|
||||
NOTIFICATION_CHANNEL_ID, notificationChannelName, NotificationManager.IMPORTANCE_MIN
|
||||
)
|
||||
channel.importance = NotificationManager.IMPORTANCE_LOW
|
||||
channel.enableLights(false)
|
||||
channel.enableVibration(false)
|
||||
channel.setShowBadge(false)
|
||||
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
private fun SessionManager.removeAndCloseAllPrivateSessions() {
|
||||
sessionsOfType(private = true).forEach { remove(it) }
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NOTIFICATION_ID = 83
|
||||
private const val NOTIFICATION_CHANNEL_ID = "browsing-session"
|
||||
private const val STARTED_FROM_PRIVATE_SHORTCUT = "STARTED_FROM_PRIVATE_SHORTCUT"
|
||||
|
||||
private const val ACTION_START = "start"
|
||||
private const val ACTION_ERASE = "erase"
|
||||
internal var started = false
|
||||
|
||||
internal fun start(
|
||||
context: Context,
|
||||
startedFromPrivateShortcut: Boolean
|
||||
) {
|
||||
val intent = Intent(context, SessionNotificationService::class.java)
|
||||
intent.action = ACTION_START
|
||||
intent.putExtra(STARTED_FROM_PRIVATE_SHORTCUT, startedFromPrivateShortcut)
|
||||
|
||||
// From Focus #2901: The application is crashing due to the service not calling `startForeground`
|
||||
// before it times out. This is a speculative fix to decrease the time between these two
|
||||
// calls by running this after potentially expensive calls in FocusApplication.onCreate and
|
||||
// BrowserFragment.inflateView by posting it to the end of the main thread.
|
||||
ThreadUtils.postToMainThread(Runnable {
|
||||
context.startService(intent)
|
||||
})
|
||||
|
||||
started = true
|
||||
}
|
||||
|
||||
internal fun stop(context: Context) {
|
||||
val intent = Intent(context, SessionNotificationService::class.java)
|
||||
|
||||
// We want to make sure we always call stop after start. So we're
|
||||
// putting these actions on the same sequential run queue.
|
||||
ThreadUtils.postToMainThread(Runnable {
|
||||
context.stopService(intent)
|
||||
})
|
||||
|
||||
started = false
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
/* 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.settings.about
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.text.util.Linkify
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import org.mozilla.fenix.R
|
||||
import java.nio.charset.Charset
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Displays the licenses of all the libraries used by Fenix.
|
||||
*
|
||||
* This is a re-implementation of play-services-oss-licenses library.
|
||||
* We can't use the official implementation in the OSS flavor of Fenix
|
||||
* because it is proprietary and closed-source.
|
||||
*
|
||||
* There are popular FLOSS alternatives to Google's plugin and library
|
||||
* such as AboutLibraries (https://github.com/mikepenz/AboutLibraries)
|
||||
* but we considered the risk of introducing such third-party dependency
|
||||
* to Fenix too high. Therefore, we use Google's gradle plugin to
|
||||
* extract the dependencies and their licenses, and this activity
|
||||
* to show the extracted licenses to the end-user.
|
||||
*/
|
||||
class AboutLibrariesActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val appName = getString(R.string.app_name)
|
||||
title = getString(R.string.open_source_licenses_title, appName)
|
||||
setContentView(R.layout.about_libraries_activity)
|
||||
|
||||
setSupportActionBar(findViewById(R.id.toolbar))
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
setupLibrariesListView()
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun setupLibrariesListView() {
|
||||
val libraries = parseLibraries()
|
||||
val listView = findViewById<ListView>(R.id.about_libraries_listview)
|
||||
listView.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, libraries)
|
||||
listView.setOnItemClickListener { _, _, position, _ ->
|
||||
showLicenseDialog(libraries[position])
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseLibraries(): List<LibraryItem> {
|
||||
/*
|
||||
The gradle plugin "oss-licenses-plugin" creates two "raw" resources:
|
||||
|
||||
- third_party_licenses which is the binary concatenation of all the licenses text for
|
||||
all the libraries. License texts can either be an URL to a license file or just the
|
||||
raw text of the license.
|
||||
|
||||
- third_party_licenses_metadata which contains one dependency per line formatted in
|
||||
the following way: "[start_offset]:[length] [name]"
|
||||
|
||||
[start_offset] : first byte in third_party_licenses that contains the license
|
||||
text for this library.
|
||||
[length] : length of the license text for this library in
|
||||
third_party_licenses.
|
||||
[name] : either the name of the library, or its artifact name.
|
||||
|
||||
See https://github.com/google/play-services-plugins/tree/master/oss-licenses-plugin
|
||||
*/
|
||||
val licensesData = resources
|
||||
.openRawResource(R.raw.third_party_licenses)
|
||||
.readBytes()
|
||||
val licensesMetadataReader = resources
|
||||
.openRawResource(R.raw.third_party_license_metadata)
|
||||
.bufferedReader()
|
||||
|
||||
return licensesMetadataReader.use { reader -> reader.readLines() }.map { line ->
|
||||
val (section, name) = line.split(" ", limit = 2)
|
||||
val (startOffset, length) = section.split(":", limit = 2).map(String::toInt)
|
||||
val licenseData = licensesData.sliceArray(startOffset until startOffset + length)
|
||||
val licenseText = licenseData.toString(Charset.forName("UTF-8"))
|
||||
LibraryItem(name, licenseText)
|
||||
}.sortedBy { item -> item.name.toLowerCase(Locale.ROOT) }
|
||||
}
|
||||
|
||||
private fun showLicenseDialog(libraryItem: LibraryItem) {
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
.setTitle(libraryItem.name)
|
||||
.setMessage(libraryItem.license)
|
||||
.create()
|
||||
dialog.show()
|
||||
|
||||
val textView = dialog.findViewById<TextView>(android.R.id.message)!!
|
||||
Linkify.addLinks(textView, Linkify.ALL)
|
||||
textView.linksClickable = true
|
||||
textView.textSize = LICENSE_TEXT_SIZE
|
||||
textView.typeface = Typeface.MONOSPACE
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val LICENSE_TEXT_SIZE = 10F
|
||||
}
|
||||
}
|
||||
|
||||
private class LibraryItem(val name: String, val license: String) {
|
||||
override fun toString(): String {
|
||||
return name
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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/. -->
|
||||
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/about_libraries"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true"
|
||||
tools:context="org.mozilla.fenix.settings.about.AboutLibrariesActivity">
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:elevation="4dp"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.ActionBar" />
|
||||
<ListView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/about_libraries_listview" />
|
||||
</RelativeLayout>
|
@ -0,0 +1,98 @@
|
||||
/* 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.library.bookmarks
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import mozilla.components.concept.menu.candidate.TextStyle
|
||||
import mozilla.components.concept.storage.BookmarkNodeType
|
||||
import mozilla.components.support.ktx.android.content.getColorFromAttr
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.mozilla.fenix.library.bookmarks.BookmarkItemMenu.Item
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class BookmarkItemMenuTest {
|
||||
|
||||
private lateinit var context: Context
|
||||
private lateinit var onItemTapped: (Item) -> Unit
|
||||
private lateinit var menu: BookmarkItemMenu
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
context = ContextThemeWrapper(testContext, R.style.NormalTheme)
|
||||
onItemTapped = mockk(relaxed = true)
|
||||
menu = BookmarkItemMenu(context, onItemTapped)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `delete item has special styling`() {
|
||||
val deleteItem = menu.menuItems(BookmarkNodeType.SEPARATOR).last()
|
||||
assertEquals("Delete", deleteItem.text)
|
||||
assertEquals(
|
||||
TextStyle(color = context.getColorFromAttr(R.attr.destructive)),
|
||||
deleteItem.textStyle
|
||||
)
|
||||
|
||||
deleteItem.onClick()
|
||||
verify { onItemTapped(Item.Delete) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `edit item appears for folders`() {
|
||||
val folderItems = menu.menuItems(BookmarkNodeType.FOLDER)
|
||||
assertEquals(2, folderItems.size)
|
||||
val (edit, delete) = folderItems
|
||||
|
||||
assertEquals("Edit", edit.text)
|
||||
edit.onClick()
|
||||
verify { onItemTapped(Item.Edit) }
|
||||
|
||||
assertEquals("Delete", delete.text)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all item appears for sites`() {
|
||||
val siteItems = menu.menuItems(BookmarkNodeType.ITEM)
|
||||
assertEquals(6, siteItems.size)
|
||||
val (edit, copy, share, openInNewTab, openInPrivateTab, delete) = siteItems
|
||||
|
||||
assertEquals("Edit", edit.text)
|
||||
assertEquals("Copy", copy.text)
|
||||
assertEquals("Share", share.text)
|
||||
assertEquals("Open in new tab", openInNewTab.text)
|
||||
assertEquals("Open in private tab", openInPrivateTab.text)
|
||||
assertEquals("Delete", delete.text)
|
||||
|
||||
edit.onClick()
|
||||
verify { onItemTapped(Item.Edit) }
|
||||
|
||||
copy.onClick()
|
||||
verify { onItemTapped(Item.Copy) }
|
||||
|
||||
share.onClick()
|
||||
verify { onItemTapped(Item.Share) }
|
||||
|
||||
openInNewTab.onClick()
|
||||
verify { onItemTapped(Item.OpenInNewTab) }
|
||||
|
||||
openInPrivateTab.onClick()
|
||||
verify { onItemTapped(Item.OpenInPrivateTab) }
|
||||
|
||||
delete.onClick()
|
||||
verify { onItemTapped(Item.Delete) }
|
||||
}
|
||||
|
||||
private operator fun <T> List<T>.component6(): T {
|
||||
return get(5)
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
/* 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.library.bookmarks.viewholders
|
||||
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.verify
|
||||
import mozilla.components.concept.storage.BookmarkNode
|
||||
import mozilla.components.concept.storage.BookmarkNodeType
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.ext.hideAndDisable
|
||||
import org.mozilla.fenix.ext.showAndEnable
|
||||
import org.mozilla.fenix.library.LibrarySiteItemView
|
||||
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentInteractor
|
||||
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentState
|
||||
import org.mozilla.fenix.library.bookmarks.BookmarkPayload
|
||||
|
||||
class BookmarkFolderViewHolderTest {
|
||||
|
||||
@MockK
|
||||
private lateinit var interactor: BookmarkFragmentInteractor
|
||||
@MockK(relaxed = true)
|
||||
private lateinit var siteItemView: LibrarySiteItemView
|
||||
private lateinit var holder: BookmarkFolderViewHolder
|
||||
|
||||
private val folder = BookmarkNode(
|
||||
type = BookmarkNodeType.FOLDER,
|
||||
guid = "456",
|
||||
parentGuid = "123",
|
||||
position = 0,
|
||||
title = "Folder",
|
||||
url = null,
|
||||
children = listOf()
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
MockKAnnotations.init(this)
|
||||
|
||||
mockkStatic(AppCompatResources::class)
|
||||
every { AppCompatResources.getDrawable(any(), any()) } returns mockk(relaxed = true)
|
||||
|
||||
holder = BookmarkFolderViewHolder(siteItemView, interactor)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `binds title and selected state`() {
|
||||
holder.bind(folder, BookmarkFragmentState.Mode.Normal())
|
||||
|
||||
verify {
|
||||
siteItemView.titleView.text = folder.title
|
||||
siteItemView.overflowView.showAndEnable()
|
||||
siteItemView.changeSelected(false)
|
||||
}
|
||||
|
||||
holder.bind(folder, BookmarkFragmentState.Mode.Selecting(setOf(folder)))
|
||||
|
||||
verify {
|
||||
siteItemView.titleView.text = folder.title
|
||||
siteItemView.overflowView.hideAndDisable()
|
||||
siteItemView.changeSelected(true)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bind with payload of no changes does not rebind views`() {
|
||||
holder.bind(
|
||||
folder,
|
||||
BookmarkFragmentState.Mode.Normal(),
|
||||
BookmarkPayload(false, false, false, false)
|
||||
)
|
||||
|
||||
verify(inverse = true) {
|
||||
siteItemView.titleView.text = folder.title
|
||||
siteItemView.overflowView.showAndEnable()
|
||||
siteItemView.overflowView.hideAndDisable()
|
||||
siteItemView.changeSelected(any())
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,144 @@
|
||||
/* 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.library.bookmarks.viewholders
|
||||
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.verify
|
||||
import mozilla.components.concept.storage.BookmarkNode
|
||||
import mozilla.components.concept.storage.BookmarkNodeType
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.ext.hideAndDisable
|
||||
import org.mozilla.fenix.ext.showAndEnable
|
||||
import org.mozilla.fenix.library.LibrarySiteItemView
|
||||
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentInteractor
|
||||
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentState
|
||||
import org.mozilla.fenix.library.bookmarks.BookmarkPayload
|
||||
|
||||
class BookmarkItemViewHolderTest {
|
||||
|
||||
@MockK
|
||||
private lateinit var interactor: BookmarkFragmentInteractor
|
||||
|
||||
@MockK(relaxed = true)
|
||||
private lateinit var siteItemView: LibrarySiteItemView
|
||||
|
||||
private lateinit var holder: BookmarkItemViewHolder
|
||||
|
||||
private val item = BookmarkNode(
|
||||
type = BookmarkNodeType.ITEM,
|
||||
guid = "456",
|
||||
parentGuid = "123",
|
||||
position = 0,
|
||||
title = "Mozilla",
|
||||
url = "https://www.mozilla.org",
|
||||
children = listOf()
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
MockKAnnotations.init(this)
|
||||
holder = BookmarkItemViewHolder(siteItemView, interactor)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `binds views for unselected item`() {
|
||||
val mode = BookmarkFragmentState.Mode.Normal()
|
||||
holder.bind(item, mode)
|
||||
|
||||
verify {
|
||||
siteItemView.setSelectionInteractor(item, mode, interactor)
|
||||
siteItemView.titleView.text = item.title
|
||||
siteItemView.urlView.text = item.url
|
||||
siteItemView.overflowView.showAndEnable()
|
||||
siteItemView.changeSelected(false)
|
||||
holder.setColorsAndIcons(item.url)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `binds views for selected item`() {
|
||||
val mode = BookmarkFragmentState.Mode.Selecting(setOf(item))
|
||||
holder.bind(item, mode)
|
||||
|
||||
verify {
|
||||
siteItemView.setSelectionInteractor(item, mode, interactor)
|
||||
siteItemView.titleView.text = item.title
|
||||
siteItemView.urlView.text = item.url
|
||||
siteItemView.overflowView.hideAndDisable()
|
||||
siteItemView.changeSelected(true)
|
||||
holder.setColorsAndIcons(item.url)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bind with payload of no changes does not rebind views`() {
|
||||
holder.bind(
|
||||
item,
|
||||
BookmarkFragmentState.Mode.Normal(),
|
||||
BookmarkPayload(false, false, false, false)
|
||||
)
|
||||
|
||||
verify(inverse = true) {
|
||||
siteItemView.titleView.text = item.title
|
||||
siteItemView.urlView.text = item.url
|
||||
siteItemView.overflowView.showAndEnable()
|
||||
siteItemView.overflowView.hideAndDisable()
|
||||
siteItemView.changeSelected(any())
|
||||
holder.setColorsAndIcons(item.url)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `binding an item with a null title uses the url as the title`() {
|
||||
val item = item.copy(title = null)
|
||||
holder.bind(item, BookmarkFragmentState.Mode.Normal())
|
||||
|
||||
verify { siteItemView.titleView.text = item.url }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `binding an item with a blank title uses the url as the title`() {
|
||||
val item = item.copy(title = " ")
|
||||
holder.bind(item, BookmarkFragmentState.Mode.Normal())
|
||||
|
||||
verify { siteItemView.titleView.text = item.url }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rebinds title if item title is null and the item url has changed`() {
|
||||
val item = item.copy(title = null)
|
||||
holder.bind(
|
||||
item,
|
||||
BookmarkFragmentState.Mode.Normal(),
|
||||
BookmarkPayload(
|
||||
titleChanged = false,
|
||||
urlChanged = true,
|
||||
selectedChanged = false,
|
||||
modeChanged = false
|
||||
)
|
||||
)
|
||||
|
||||
verify { siteItemView.titleView.text = item.url }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rebinds title if item title is blank and the item url has changed`() {
|
||||
val item = item.copy(title = " ")
|
||||
holder.bind(
|
||||
item,
|
||||
BookmarkFragmentState.Mode.Normal(),
|
||||
BookmarkPayload(
|
||||
titleChanged = false,
|
||||
urlChanged = true,
|
||||
selectedChanged = false,
|
||||
modeChanged = false
|
||||
)
|
||||
)
|
||||
|
||||
verify { siteItemView.titleView.text = item.url }
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
/* 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.library.history
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import mozilla.components.concept.menu.candidate.TextStyle
|
||||
import mozilla.components.support.ktx.android.content.getColorFromAttr
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.mozilla.fenix.library.history.HistoryItemMenu.Item
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class HistoryItemMenuTest {
|
||||
|
||||
private lateinit var context: Context
|
||||
private lateinit var onItemTapped: (Item) -> Unit
|
||||
private lateinit var menu: HistoryItemMenu
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
context = ContextThemeWrapper(testContext, R.style.NormalTheme)
|
||||
onItemTapped = mockk(relaxed = true)
|
||||
menu = HistoryItemMenu(context, onItemTapped)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `delete item has special styling`() {
|
||||
val deleteItem = menu.menuItems().last()
|
||||
assertEquals("Delete", deleteItem.text)
|
||||
assertEquals(
|
||||
TextStyle(color = context.getColorFromAttr(R.attr.destructive)),
|
||||
deleteItem.textStyle
|
||||
)
|
||||
|
||||
deleteItem.onClick()
|
||||
verify { onItemTapped(Item.Delete) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `builds menu items`() {
|
||||
val items = menu.menuItems()
|
||||
assertEquals(5, items.size)
|
||||
val (copy, share, openInNewTab, openInPrivateTab, delete) = items
|
||||
|
||||
assertEquals("Copy", copy.text)
|
||||
assertEquals("Share", share.text)
|
||||
assertEquals("Open in new tab", openInNewTab.text)
|
||||
assertEquals("Open in private tab", openInPrivateTab.text)
|
||||
assertEquals("Delete", delete.text)
|
||||
|
||||
copy.onClick()
|
||||
verify { onItemTapped(Item.Copy) }
|
||||
|
||||
share.onClick()
|
||||
verify { onItemTapped(Item.Share) }
|
||||
|
||||
openInNewTab.onClick()
|
||||
verify { onItemTapped(Item.OpenInNewTab) }
|
||||
|
||||
openInPrivateTab.onClick()
|
||||
verify { onItemTapped(Item.OpenInPrivateTab) }
|
||||
|
||||
delete.onClick()
|
||||
verify { onItemTapped(Item.Delete) }
|
||||
}
|
||||
}
|
@ -0,0 +1,320 @@
|
||||
/* 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.searchdialog
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDirections
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.unmockkObject
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import mozilla.components.browser.search.SearchEngine
|
||||
import mozilla.components.browser.session.Session
|
||||
import mozilla.components.browser.session.SessionManager
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.BrowserDirection
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.components.metrics.MetricController
|
||||
import org.mozilla.fenix.components.metrics.MetricsUtils
|
||||
import org.mozilla.fenix.ext.navigateSafe
|
||||
import org.mozilla.fenix.search.SearchFragmentAction
|
||||
import org.mozilla.fenix.settings.SupportUtils
|
||||
import org.mozilla.fenix.utils.Settings
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
class SearchDialogControllerTest {
|
||||
|
||||
@MockK(relaxed = true) private lateinit var activity: HomeActivity
|
||||
@MockK(relaxed = true) private lateinit var store: SearchDialogFragmentStore
|
||||
@MockK(relaxed = true) private lateinit var navController: NavController
|
||||
@MockK private lateinit var searchEngine: SearchEngine
|
||||
@MockK(relaxed = true) private lateinit var metrics: MetricController
|
||||
@MockK(relaxed = true) private lateinit var settings: Settings
|
||||
@MockK private lateinit var sessionManager: SessionManager
|
||||
@MockK(relaxed = true) private lateinit var clearToolbarFocus: () -> Unit
|
||||
|
||||
private lateinit var controller: SearchDialogController
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockKAnnotations.init(this)
|
||||
mockkObject(MetricsUtils)
|
||||
|
||||
every { store.state.tabId } returns "test-tab-id"
|
||||
every { store.state.searchEngineSource.searchEngine } returns searchEngine
|
||||
every { sessionManager.select(any()) } just Runs
|
||||
every { MetricsUtils.createSearchEvent(searchEngine, activity, any()) } returns null
|
||||
|
||||
controller = SearchDialogController(
|
||||
activity = activity,
|
||||
sessionManager = sessionManager,
|
||||
store = store,
|
||||
navController = navController,
|
||||
settings = settings,
|
||||
metrics = metrics,
|
||||
clearToolbarFocus = clearToolbarFocus
|
||||
)
|
||||
}
|
||||
|
||||
@After
|
||||
fun teardown() {
|
||||
unmockkObject(MetricsUtils)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleUrlCommitted() {
|
||||
val url = "https://www.google.com/"
|
||||
|
||||
controller.handleUrlCommitted(url)
|
||||
|
||||
verify {
|
||||
activity.openToBrowserAndLoad(
|
||||
searchTermOrURL = url,
|
||||
newTab = false,
|
||||
from = BrowserDirection.FromSearchDialog,
|
||||
engine = searchEngine
|
||||
)
|
||||
}
|
||||
verify { metrics.track(Event.EnteredUrl(false)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSearchCommitted() {
|
||||
val searchTerm = "Firefox"
|
||||
|
||||
controller.handleUrlCommitted(searchTerm)
|
||||
|
||||
verify {
|
||||
activity.openToBrowserAndLoad(
|
||||
searchTermOrURL = searchTerm,
|
||||
newTab = false,
|
||||
from = BrowserDirection.FromSearchDialog,
|
||||
engine = searchEngine
|
||||
)
|
||||
}
|
||||
verify { settings.incrementActiveSearchCount() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleCrashesUrlCommitted() {
|
||||
val url = "about:crashes"
|
||||
every { activity.packageName } returns "org.mozilla.fenix"
|
||||
|
||||
controller.handleUrlCommitted(url)
|
||||
|
||||
verify {
|
||||
activity.startActivity(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleMozillaUrlCommitted() {
|
||||
val url = "moz://a"
|
||||
|
||||
controller.handleUrlCommitted(url)
|
||||
|
||||
verify {
|
||||
activity.openToBrowserAndLoad(
|
||||
searchTermOrURL = SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.MANIFESTO),
|
||||
newTab = false,
|
||||
from = BrowserDirection.FromSearchDialog,
|
||||
engine = searchEngine
|
||||
)
|
||||
}
|
||||
verify { metrics.track(Event.EnteredUrl(false)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleEditingCancelled() = runBlockingTest {
|
||||
controller.handleEditingCancelled()
|
||||
|
||||
verify {
|
||||
clearToolbarFocus()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleTextChangedNonEmpty() {
|
||||
val text = "fenix"
|
||||
|
||||
controller.handleTextChanged(text)
|
||||
|
||||
verify { store.dispatch(SearchFragmentAction.UpdateQuery(text)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleTextChangedEmpty() {
|
||||
val text = ""
|
||||
|
||||
controller.handleTextChanged(text)
|
||||
|
||||
verify { store.dispatch(SearchFragmentAction.UpdateQuery(text)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `show search shortcuts when setting enabled AND query empty`() {
|
||||
val text = ""
|
||||
every { settings.shouldShowSearchShortcuts } returns true
|
||||
|
||||
controller.handleTextChanged(text)
|
||||
|
||||
verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(true)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `show search shortcuts when setting enabled AND query equals url`() {
|
||||
val text = "mozilla.org"
|
||||
every { store.state.url } returns "mozilla.org"
|
||||
every { settings.shouldShowSearchShortcuts } returns true
|
||||
|
||||
controller.handleTextChanged(text)
|
||||
|
||||
verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(true)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `do not show search shortcuts when setting enabled AND query non-empty`() {
|
||||
val text = "mozilla"
|
||||
|
||||
controller.handleTextChanged(text)
|
||||
|
||||
verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(false)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `do not show search shortcuts when setting disabled AND query empty AND url not matching query`() {
|
||||
every { settings.shouldShowSearchShortcuts } returns false
|
||||
|
||||
val text = ""
|
||||
|
||||
controller.handleTextChanged(text)
|
||||
|
||||
verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(false)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `do not show search shortcuts when setting disabled AND query non-empty`() {
|
||||
every { settings.shouldShowSearchShortcuts } returns false
|
||||
|
||||
val text = "mozilla"
|
||||
|
||||
controller.handleTextChanged(text)
|
||||
|
||||
verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(false)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleUrlTapped() {
|
||||
val url = "https://www.google.com/"
|
||||
|
||||
controller.handleUrlTapped(url)
|
||||
|
||||
verify {
|
||||
activity.openToBrowserAndLoad(
|
||||
searchTermOrURL = url,
|
||||
newTab = false,
|
||||
from = BrowserDirection.FromSearchDialog
|
||||
)
|
||||
}
|
||||
verify { metrics.track(Event.EnteredUrl(false)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSearchTermsTapped() {
|
||||
val searchTerms = "fenix"
|
||||
|
||||
controller.handleSearchTermsTapped(searchTerms)
|
||||
|
||||
verify {
|
||||
activity.openToBrowserAndLoad(
|
||||
searchTermOrURL = searchTerms,
|
||||
newTab = false,
|
||||
from = BrowserDirection.FromSearchDialog,
|
||||
engine = searchEngine,
|
||||
forceSearch = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSearchShortcutEngineSelected() {
|
||||
val searchEngine: SearchEngine = mockk(relaxed = true)
|
||||
|
||||
controller.handleSearchShortcutEngineSelected(searchEngine)
|
||||
|
||||
verify { store.dispatch(SearchFragmentAction.SearchShortcutEngineSelected(searchEngine)) }
|
||||
verify { metrics.track(Event.SearchShortcutSelected(searchEngine, false)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleClickSearchEngineSettings() {
|
||||
val directions: NavDirections =
|
||||
SearchDialogFragmentDirections.actionGlobalSearchEngineFragment()
|
||||
|
||||
controller.handleClickSearchEngineSettings()
|
||||
|
||||
verify { navController.navigateSafe(R.id.searchEngineFragment, directions) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSearchShortcutsButtonClicked_alreadyOpen() {
|
||||
every { store.state.showSearchShortcuts } returns true
|
||||
|
||||
controller.handleSearchShortcutsButtonClicked()
|
||||
|
||||
verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(false)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSearchShortcutsButtonClicked_notYetOpen() {
|
||||
every { store.state.showSearchShortcuts } returns false
|
||||
|
||||
controller.handleSearchShortcutsButtonClicked()
|
||||
|
||||
verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(true)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleExistingSessionSelected() {
|
||||
val session = mockk<Session>()
|
||||
|
||||
controller.handleExistingSessionSelected(session)
|
||||
|
||||
verify { sessionManager.select(session) }
|
||||
verify { activity.openToBrowser(from = BrowserDirection.FromSearchDialog) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleExistingSessionSelected_tabId_nullSession() {
|
||||
every { sessionManager.findSessionById("tab-id") } returns null
|
||||
|
||||
controller.handleExistingSessionSelected("tab-id")
|
||||
|
||||
verify(inverse = true) { sessionManager.select(any()) }
|
||||
verify(inverse = true) { activity.openToBrowser(from = BrowserDirection.FromSearchDialog) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleExistingSessionSelected_tabId() {
|
||||
val session = mockk<Session>()
|
||||
every { sessionManager.findSessionById("tab-id") } returns session
|
||||
|
||||
controller.handleExistingSessionSelected("tab-id")
|
||||
|
||||
verify { sessionManager.select(any()) }
|
||||
verify { activity.openToBrowser(from = BrowserDirection.FromSearchDialog) }
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
/* 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.session
|
||||
|
||||
import android.content.Context
|
||||
import io.mockk.Called
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.confirmVerified
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import mozilla.components.browser.state.action.CustomTabListAction
|
||||
import mozilla.components.browser.state.action.TabListAction
|
||||
import mozilla.components.browser.state.state.createCustomTab
|
||||
import mozilla.components.browser.state.state.createTab
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class NotificationSessionObserverTest {
|
||||
|
||||
private lateinit var observer: NotificationSessionObserver
|
||||
private lateinit var store: BrowserStore
|
||||
@MockK private lateinit var context: Context
|
||||
@MockK(relaxed = true) private lateinit var notificationService: SessionNotificationService.Companion
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
MockKAnnotations.init(this)
|
||||
store = BrowserStore()
|
||||
every { context.components.core.store } returns store
|
||||
observer = NotificationSessionObserver(context, notificationService)
|
||||
NotificationSessionObserver.isStartedFromPrivateShortcut = false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN session is private and non-custom WHEN it is added THEN notification service should be started`() = runBlocking {
|
||||
val privateSession = createTab("https://firefox.com", private = true)
|
||||
|
||||
store.dispatch(TabListAction.AddTabAction(privateSession)).join()
|
||||
|
||||
observer.start()
|
||||
verify(exactly = 1) { notificationService.start(context, false) }
|
||||
confirmVerified(notificationService)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN session is not private WHEN it is added THEN notification service should not be started`() = runBlocking {
|
||||
val normalSession = createTab("https://firefox.com")
|
||||
val customSession = createCustomTab("https://firefox.com")
|
||||
|
||||
observer.start()
|
||||
verify { notificationService wasNot Called }
|
||||
|
||||
store.dispatch(TabListAction.AddTabAction(normalSession)).join()
|
||||
verify(exactly = 0) { notificationService.start(context, false) }
|
||||
|
||||
store.dispatch(CustomTabListAction.AddCustomTabAction(customSession)).join()
|
||||
verify(exactly = 0) { notificationService.start(context, false) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN session is custom tab WHEN it is added THEN notification service should not be started`() = runBlocking {
|
||||
val privateCustomSession = createCustomTab("https://firefox.com").let {
|
||||
it.copy(content = it.content.copy(private = true))
|
||||
}
|
||||
val customSession = createCustomTab("https://firefox.com")
|
||||
|
||||
observer.start()
|
||||
verify { notificationService wasNot Called }
|
||||
|
||||
store.dispatch(CustomTabListAction.AddCustomTabAction(privateCustomSession)).join()
|
||||
verify(exactly = 0) { notificationService.start(context, false) }
|
||||
|
||||
store.dispatch(CustomTabListAction.AddCustomTabAction(customSession)).join()
|
||||
verify(exactly = 0) { notificationService.start(context, false) }
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
/* 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.session
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import mozilla.components.feature.privatemode.notification.AbstractPrivateNotificationService.Companion.ACTION_ERASE
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.HomeActivity.Companion.PRIVATE_BROWSING_MODE
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.robolectric.Robolectric
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.android.controller.ServiceController
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class PrivateNotificationServiceTest {
|
||||
|
||||
private lateinit var controller: ServiceController<PrivateNotificationService>
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val store = testContext.components.core.store
|
||||
every { store.dispatch(any()) } returns mockk()
|
||||
|
||||
controller = Robolectric.buildService(
|
||||
PrivateNotificationService::class.java,
|
||||
Intent(ACTION_ERASE)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `service opens home activity with PBM flag set to true`() {
|
||||
PrivateNotificationService.isStartedFromPrivateShortcut = true
|
||||
val service = shadowOf(controller.get())
|
||||
controller.startCommand(0, 0)
|
||||
|
||||
val intent = service.nextStartedActivity
|
||||
assertEquals(ComponentName(testContext, HomeActivity::class.java), intent.component)
|
||||
assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK, intent.flags)
|
||||
assertEquals(true, intent.extras?.getBoolean(PRIVATE_BROWSING_MODE))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `service opens home activity with PBM flag set to false`() {
|
||||
PrivateNotificationService.isStartedFromPrivateShortcut = false
|
||||
val service = shadowOf(controller.get())
|
||||
controller.startCommand(0, 0)
|
||||
|
||||
val intent = service.nextStartedActivity
|
||||
assertEquals(ComponentName(testContext, HomeActivity::class.java), intent.component)
|
||||
assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK, intent.flags)
|
||||
assertEquals(false, intent.extras?.getBoolean(PRIVATE_BROWSING_MODE))
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
/* 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.session
|
||||
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class SessionNotificationServiceTest {
|
||||
|
||||
@Test
|
||||
fun `Service keeps tracked of started state`() {
|
||||
assertFalse(SessionNotificationService.started)
|
||||
|
||||
SessionNotificationService.start(testContext, false)
|
||||
assertTrue(SessionNotificationService.started)
|
||||
|
||||
SessionNotificationService.stop(testContext)
|
||||
assertFalse(SessionNotificationService.started)
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/* 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.settings.about
|
||||
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.robolectric.Robolectric
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.shadows.ShadowAlertDialog
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class AboutLibrariesActivityTest {
|
||||
@Test
|
||||
fun `activity should display licenses`() {
|
||||
val activity = Robolectric.buildActivity(AboutLibrariesActivity::class.java).create().get()
|
||||
val listView = activity.findViewById<ListView>(R.id.about_libraries_listview)
|
||||
|
||||
assertTrue(0 < listView.count)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `item click should open license dialog`() {
|
||||
val activity = Robolectric.buildActivity(AboutLibrariesActivity::class.java).create().get()
|
||||
|
||||
val listView = activity.findViewById<ListView>(R.id.about_libraries_listview)
|
||||
val listViewShadow = shadowOf(listView)
|
||||
listViewShadow.clickFirstItemContainingText("org.mozilla.geckoview:geckoview")
|
||||
|
||||
val alertDialogShadow = ShadowAlertDialog.getLatestDialog()
|
||||
assertTrue(alertDialogShadow.isShowing)
|
||||
|
||||
val alertDialogText = alertDialogShadow
|
||||
.findViewById<TextView>(android.R.id.message)
|
||||
.text
|
||||
.toString()
|
||||
assertTrue(alertDialogText.contains("MPL"))
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue