1
0
Fork 0

Closes #12522: Reuse exceptions code (#13047)

master
Tiger Oakes 2020-07-31 13:24:14 -07:00 committed by GitHub
parent 8b923fc7a4
commit 1d28f63737
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 1116 additions and 1144 deletions

View File

@ -67,6 +67,7 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.browser.browsingmode.DefaultBrowsingModeManager
import org.mozilla.fenix.components.metrics.BreadcrumbsRecorder
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExceptionsFragmentDirections
import org.mozilla.fenix.ext.alreadyOnDestination
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
@ -98,7 +99,6 @@ import org.mozilla.fenix.sync.SyncedTabsFragmentDirections
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
import org.mozilla.fenix.theme.DefaultThemeManager
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.trackingprotectionexceptions.TrackingProtectionExceptionsFragmentDirections
import org.mozilla.fenix.utils.BrowsersCache
/**

View File

@ -0,0 +1,100 @@
/* 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.exceptions
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.annotation.StringRes
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.mozilla.fenix.exceptions.viewholders.ExceptionsDeleteButtonViewHolder
import org.mozilla.fenix.exceptions.viewholders.ExceptionsHeaderViewHolder
import org.mozilla.fenix.exceptions.viewholders.ExceptionsListItemViewHolder
/**
* Adapter for a list of sites that are exempted from saving logins or tracking protection,
* along with controls to remove the exception.
*/
abstract class ExceptionsAdapter<T : Any>(
private val interactor: ExceptionsInteractor<T>,
diffCallback: DiffUtil.ItemCallback<AdapterItem>
) : ListAdapter<ExceptionsAdapter.AdapterItem, RecyclerView.ViewHolder>(diffCallback) {
/**
* Change the list of items that are displayed.
* Header and footer items are added to the list as well.
*/
fun updateData(exceptions: List<T>) {
val adapterItems: List<AdapterItem> = listOf(AdapterItem.Header) +
exceptions.map { wrapAdapterItem(it) } +
listOf(AdapterItem.DeleteButton)
submitList(adapterItems)
}
/**
* Layout to use for the delete button.
*/
@get:LayoutRes
abstract val deleteButtonLayoutId: Int
/**
* String to use for the exceptions list header.
*/
@get:StringRes
abstract val headerDescriptionResource: Int
/**
* Converts an item from [updateData] into an adapter item.
*/
abstract fun wrapAdapterItem(item: T): AdapterItem.Item<T>
final override fun getItemViewType(position: Int) = when (getItem(position)) {
AdapterItem.DeleteButton -> deleteButtonLayoutId
AdapterItem.Header -> ExceptionsHeaderViewHolder.LAYOUT_ID
is AdapterItem.Item<*> -> ExceptionsListItemViewHolder.LAYOUT_ID
}
final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return when (viewType) {
deleteButtonLayoutId ->
ExceptionsDeleteButtonViewHolder(view, interactor)
ExceptionsHeaderViewHolder.LAYOUT_ID ->
ExceptionsHeaderViewHolder(view, headerDescriptionResource)
ExceptionsListItemViewHolder.LAYOUT_ID ->
ExceptionsListItemViewHolder(view, interactor)
else -> throw IllegalStateException()
}
}
@Suppress("Unchecked_Cast")
final override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is ExceptionsListItemViewHolder<*>) {
holder as ExceptionsListItemViewHolder<T>
val adapterItem = getItem(position) as AdapterItem.Item<T>
holder.bind(adapterItem.item, adapterItem.url)
}
}
/**
* Internal items for [ExceptionsAdapter]
*/
sealed class AdapterItem {
object DeleteButton : AdapterItem()
object Header : AdapterItem()
/**
* Represents an item to display in [ExceptionsAdapter].
* [T] should refer to the same value as in the [ExceptionsAdapter] and [ExceptionsInteractor].
*/
abstract class Item<T> : AdapterItem() {
abstract val item: T
abstract val url: String
}
}
}

View File

@ -0,0 +1,21 @@
/* 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.exceptions
/**
* Interface for exceptions view interactors. This interface is implemented by objects that want
* to respond to user interaction on the [ExceptionsView].
*/
interface ExceptionsInteractor<T> {
/**
* Called whenever all exception items are deleted
*/
fun onDeleteAll()
/**
* Called whenever one exception item is deleted
*/
fun onDeleteOne(item: T)
}

View File

@ -0,0 +1,41 @@
/* 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.exceptions
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.component_exceptions.*
import org.mozilla.fenix.R
/**
* View that contains and configures the Exceptions List
*/
abstract class ExceptionsView<T : Any>(
container: ViewGroup,
protected val interactor: ExceptionsInteractor<T>
) : LayoutContainer {
override val containerView: FrameLayout = LayoutInflater.from(container.context)
.inflate(R.layout.component_exceptions, container, true)
.findViewById(R.id.exceptions_wrapper)
protected abstract val exceptionsAdapter: ExceptionsAdapter<T>
init {
exceptions_list.apply {
layoutManager = LinearLayoutManager(containerView.context)
}
}
fun update(items: List<T>) {
exceptions_empty_view.isVisible = items.isEmpty()
exceptions_list.isVisible = items.isNotEmpty()
exceptionsAdapter.updateData(items)
}
}

View File

@ -2,7 +2,7 @@
* 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.loginexceptions
package org.mozilla.fenix.exceptions.login
import mozilla.components.feature.logins.exceptions.LoginException
import mozilla.components.lib.state.Action
@ -26,7 +26,7 @@ sealed class ExceptionsFragmentAction : Action {
* The state for the Exceptions Screen
* @property items List of exceptions to display
*/
data class ExceptionsFragmentState(val items: List<LoginException>) : State
data class ExceptionsFragmentState(val items: List<LoginException> = emptyList()) : State
/**
* The ExceptionsState Reducer.

View File

@ -0,0 +1,44 @@
/* 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.exceptions.login
import androidx.recyclerview.widget.DiffUtil
import mozilla.components.feature.logins.exceptions.LoginException
import org.mozilla.fenix.R
import org.mozilla.fenix.exceptions.ExceptionsAdapter
/**
* Adapter for a list of sites that are exempted from saving logins,
* along with controls to remove the exception.
*/
class LoginExceptionsAdapter(
interactor: LoginExceptionsInteractor
) : ExceptionsAdapter<LoginException>(interactor, DiffCallback) {
override val deleteButtonLayoutId = R.layout.delete_logins_exceptions_button
override val headerDescriptionResource = R.string.preferences_passwords_exceptions_description
override fun wrapAdapterItem(item: LoginException) =
LoginAdapterItem(item)
data class LoginAdapterItem(
override val item: LoginException
) : AdapterItem.Item<LoginException>() {
override val url get() = item.origin
}
internal object DiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
override fun areItemsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
when (oldItem) {
AdapterItem.DeleteButton, AdapterItem.Header -> oldItem === newItem
is LoginAdapterItem -> newItem is LoginAdapterItem && oldItem.item.id == newItem.item.id
else -> false
}
@Suppress("DiffUtilEquals")
override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
oldItem == newItem
}
}

View File

@ -2,7 +2,7 @@
* 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.loginexceptions
package org.mozilla.fenix.exceptions.login
import android.os.Bundle
import android.view.LayoutInflater
@ -13,10 +13,9 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.observe
import kotlinx.android.synthetic.main.fragment_exceptions.view.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import mozilla.components.feature.logins.exceptions.LoginException
import kotlinx.coroutines.plus
import mozilla.components.lib.state.ext.consumeFrom
import org.mozilla.fenix.R
import org.mozilla.fenix.components.StoreProvider
@ -45,14 +44,17 @@ class LoginExceptionsFragment : Fragment() {
val view = inflater.inflate(R.layout.fragment_exceptions, container, false)
exceptionsStore = StoreProvider.get(this) {
ExceptionsFragmentStore(
ExceptionsFragmentState(
items = listOf()
)
ExceptionsFragmentState(items = emptyList())
)
}
exceptionsInteractor =
LoginExceptionsInteractor(::deleteOneItem, ::deleteAllItems)
exceptionsView = LoginExceptionsView(view.exceptionsLayout, exceptionsInteractor)
exceptionsInteractor = DefaultLoginExceptionsInteractor(
ioScope = viewLifecycleOwner.lifecycleScope + Dispatchers.IO,
loginExceptionStorage = requireComponents.core.loginExceptionStorage
)
exceptionsView = LoginExceptionsView(
view.exceptionsLayout,
exceptionsInteractor
)
subscribeToLoginExceptions()
return view
}
@ -67,19 +69,7 @@ class LoginExceptionsFragment : Fragment() {
@ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
consumeFrom(exceptionsStore) {
exceptionsView.update(it)
}
}
private fun deleteAllItems() {
viewLifecycleOwner.lifecycleScope.launch(IO) {
requireComponents.core.loginExceptionStorage.deleteAllLoginExceptions()
}
}
private fun deleteOneItem(item: LoginException) {
viewLifecycleOwner.lifecycleScope.launch(IO) {
requireComponents.core.loginExceptionStorage.removeLoginException(item)
exceptionsView.update(it.items)
}
}
}

View File

@ -0,0 +1,31 @@
/* 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.exceptions.login
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import mozilla.components.feature.logins.exceptions.LoginException
import mozilla.components.feature.logins.exceptions.LoginExceptionStorage
import org.mozilla.fenix.exceptions.ExceptionsInteractor
interface LoginExceptionsInteractor : ExceptionsInteractor<LoginException>
class DefaultLoginExceptionsInteractor(
private val ioScope: CoroutineScope,
private val loginExceptionStorage: LoginExceptionStorage
) : LoginExceptionsInteractor {
override fun onDeleteAll() {
ioScope.launch {
loginExceptionStorage.deleteAllLoginExceptions()
}
}
override fun onDeleteOne(item: LoginException) {
ioScope.launch {
loginExceptionStorage.removeLoginException(item)
}
}
}

View File

@ -0,0 +1,29 @@
/* 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.exceptions.login
import android.view.ViewGroup
import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.component_exceptions.*
import mozilla.components.feature.logins.exceptions.LoginException
import org.mozilla.fenix.R
import org.mozilla.fenix.exceptions.ExceptionsView
class LoginExceptionsView(
container: ViewGroup,
interactor: LoginExceptionsInteractor
) : ExceptionsView<LoginException>(container, interactor) {
override val exceptionsAdapter = LoginExceptionsAdapter(interactor)
init {
exceptions_learn_more.isVisible = false
exceptions_empty_message.text =
containerView.context.getString(R.string.preferences_passwords_exceptions_description_empty)
exceptions_list.apply {
adapter = exceptionsAdapter
}
}
}

View File

@ -2,19 +2,13 @@
* 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.trackingprotectionexceptions
package org.mozilla.fenix.exceptions.trackingprotection
import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
/**
* Class representing an exception item
* @property url Host of the exception
*/
data class ExceptionItem(override val url: String) : TrackingProtectionException
/**
* The [Store] for holding the [ExceptionsFragmentState] and applying [ExceptionsFragmentAction]s.
*/
@ -32,7 +26,7 @@ sealed class ExceptionsFragmentAction : Action {
* The state for the Exceptions Screen
* @property items List of exceptions to display
*/
data class ExceptionsFragmentState(val items: List<TrackingProtectionException>) : State
data class ExceptionsFragmentState(val items: List<TrackingProtectionException> = emptyList()) : State
/**
* The ExceptionsState Reducer.

View File

@ -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.exceptions.trackingprotection
import androidx.recyclerview.widget.DiffUtil
import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
import org.mozilla.fenix.R
import org.mozilla.fenix.exceptions.ExceptionsAdapter
/**
* Adapter for a list of sites that are exempted from Tracking Protection,
* along with controls to remove the exception.
*/
class TrackingProtectionExceptionsAdapter(
interactor: TrackingProtectionExceptionsInteractor
) : ExceptionsAdapter<TrackingProtectionException>(interactor, DiffCallback) {
override val deleteButtonLayoutId = R.layout.delete_exceptions_button
override val headerDescriptionResource = R.string.enhanced_tracking_protection_exceptions
override fun wrapAdapterItem(item: TrackingProtectionException) =
TrackingProtectionAdapterItem(item)
data class TrackingProtectionAdapterItem(
override val item: TrackingProtectionException
) : AdapterItem.Item<TrackingProtectionException>() {
override val url get() = item.url
}
internal object DiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
override fun areItemsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
when (oldItem) {
AdapterItem.DeleteButton, AdapterItem.Header -> oldItem === newItem
is TrackingProtectionAdapterItem ->
newItem is TrackingProtectionAdapterItem && oldItem.item.url == newItem.item.url
else -> false
}
@Suppress("DiffUtilEquals")
override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
oldItem == newItem
}
}

View File

@ -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.exceptions.trackingprotection
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import kotlinx.android.synthetic.main.fragment_exceptions.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.lib.state.ext.consumeFrom
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.showToolbar
/**
* Displays a list of sites that are exempted from Tracking Protection,
* along with controls to remove the exception.
*/
class TrackingProtectionExceptionsFragment : Fragment() {
private lateinit var exceptionsStore: ExceptionsFragmentStore
private lateinit var exceptionsView: TrackingProtectionExceptionsView
private lateinit var exceptionsInteractor: DefaultTrackingProtectionExceptionsInteractor
override fun onResume() {
super.onResume()
showToolbar(getString(R.string.preference_exceptions))
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_exceptions, container, false)
exceptionsStore = StoreProvider.get(this) {
ExceptionsFragmentStore(
ExceptionsFragmentState(items = emptyList())
)
}
exceptionsInteractor = DefaultTrackingProtectionExceptionsInteractor(
activity = activity as HomeActivity,
exceptionsStore = exceptionsStore,
trackingProtectionUseCases = requireComponents.useCases.trackingProtectionUseCases
)
exceptionsView = TrackingProtectionExceptionsView(
view.exceptionsLayout,
exceptionsInteractor
)
exceptionsInteractor.reloadExceptions()
return view
}
@ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
consumeFrom(exceptionsStore) {
exceptionsView.update(it.items)
}
}
}

View File

@ -0,0 +1,54 @@
/* 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.exceptions.trackingprotection
import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
import mozilla.components.feature.session.TrackingProtectionUseCases
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.exceptions.ExceptionsInteractor
import org.mozilla.fenix.settings.SupportUtils
interface TrackingProtectionExceptionsInteractor : ExceptionsInteractor<TrackingProtectionException> {
/**
* Called whenever learn more about tracking protection is tapped
*/
fun onLearnMore()
}
class DefaultTrackingProtectionExceptionsInteractor(
private val activity: HomeActivity,
private val exceptionsStore: ExceptionsFragmentStore,
private val trackingProtectionUseCases: TrackingProtectionUseCases
) : TrackingProtectionExceptionsInteractor {
override fun onLearnMore() {
activity.openToBrowserAndLoad(
searchTermOrURL = SupportUtils.getGenericSumoURLForTopic(
SupportUtils.SumoTopic.TRACKING_PROTECTION
),
newTab = true,
from = BrowserDirection.FromTrackingProtectionExceptions
)
}
override fun onDeleteAll() {
trackingProtectionUseCases.removeAllExceptions()
reloadExceptions()
}
override fun onDeleteOne(item: TrackingProtectionException) {
trackingProtectionUseCases.removeException(item)
reloadExceptions()
}
fun reloadExceptions() {
trackingProtectionUseCases.fetchExceptions { resultList ->
exceptionsStore.dispatch(
ExceptionsFragmentAction.Change(resultList)
)
}
}
}

View File

@ -0,0 +1,33 @@
/* 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.exceptions.trackingprotection
import android.text.method.LinkMovementMethod
import android.view.ViewGroup
import kotlinx.android.synthetic.main.component_exceptions.*
import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
import org.mozilla.fenix.exceptions.ExceptionsView
import org.mozilla.fenix.ext.addUnderline
class TrackingProtectionExceptionsView(
container: ViewGroup,
interactor: TrackingProtectionExceptionsInteractor
) : ExceptionsView<TrackingProtectionException>(container, interactor) {
override val exceptionsAdapter = TrackingProtectionExceptionsAdapter(interactor)
init {
exceptions_list.apply {
adapter = exceptionsAdapter
}
with(exceptions_learn_more) {
addUnderline()
movementMethod = LinkMovementMethod.getInstance()
setOnClickListener { interactor.onLearnMore() }
}
}
}

View File

@ -2,27 +2,23 @@
* 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.trackingprotectionexceptions.viewholders
package org.mozilla.fenix.exceptions.viewholders
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.delete_exceptions_button.view.*
import com.google.android.material.button.MaterialButton
import org.mozilla.fenix.R
import org.mozilla.fenix.trackingprotectionexceptions.ExceptionsInteractor
import org.mozilla.fenix.exceptions.ExceptionsInteractor
class ExceptionsDeleteButtonViewHolder(
view: View,
private val interactor: ExceptionsInteractor
private val interactor: ExceptionsInteractor<*>
) : RecyclerView.ViewHolder(view) {
private val deleteButton = view.removeAllExceptions
init {
val deleteButton: MaterialButton = view.findViewById(R.id.removeAllExceptions)
deleteButton.setOnClickListener {
interactor.onDeleteAll()
}
}
companion object {
const val LAYOUT_ID = R.layout.delete_exceptions_button
}
}

View File

@ -2,20 +2,21 @@
* 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.loginexceptions.viewholders
package org.mozilla.fenix.exceptions.viewholders
import android.view.View
import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.exceptions_description.view.*
import org.mozilla.fenix.R
class LoginExceptionsHeaderViewHolder(
view: View
class ExceptionsHeaderViewHolder(
view: View,
@StringRes description: Int
) : RecyclerView.ViewHolder(view) {
init {
view.exceptions_description.text =
view.context.getString(R.string.preferences_passwords_exceptions_description)
view.exceptions_description.text = view.context.getString(description)
}
companion object {

View File

@ -0,0 +1,42 @@
/* 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.exceptions.viewholders
import android.view.View
import kotlinx.android.synthetic.main.exception_item.*
import mozilla.components.browser.icons.BrowserIcons
import org.mozilla.fenix.R
import org.mozilla.fenix.exceptions.ExceptionsInteractor
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.loadIntoView
import org.mozilla.fenix.utils.view.ViewHolder
/**
* View holder for a single website that is exempted from Tracking Protection or Logins.
*/
class ExceptionsListItemViewHolder<T : Any>(
view: View,
private val interactor: ExceptionsInteractor<T>,
private val icons: BrowserIcons = view.context.components.core.icons
) : ViewHolder(view) {
private lateinit var item: T
init {
delete_exception.setOnClickListener {
interactor.onDeleteOne(item)
}
}
fun bind(item: T, url: String) {
this.item = item
webAddressView.text = url
icons.loadIntoView(favicon_image, url)
}
companion object {
const val LAYOUT_ID = R.layout.exception_item
}
}

View File

@ -1,83 +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.loginexceptions
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.feature.logins.exceptions.LoginException
import org.mozilla.fenix.loginexceptions.viewholders.LoginExceptionsDeleteButtonViewHolder
import org.mozilla.fenix.loginexceptions.viewholders.LoginExceptionsHeaderViewHolder
import org.mozilla.fenix.loginexceptions.viewholders.LoginExceptionsListItemViewHolder
/**
* Adapter for a list of sites that are exempted from saving logins,
* along with controls to remove the exception.
*/
class LoginExceptionsAdapter(
private val interactor: LoginExceptionsInteractor
) : ListAdapter<LoginExceptionsAdapter.AdapterItem, RecyclerView.ViewHolder>(DiffCallback) {
/**
* Change the list of items that are displayed.
* Header and footer items are added to the list as well.
*/
fun updateData(exceptions: List<LoginException>) {
val adapterItems: List<AdapterItem> = listOf(AdapterItem.Header) +
exceptions.map { AdapterItem.Item(it) } +
listOf(AdapterItem.DeleteButton)
submitList(adapterItems)
}
override fun getItemViewType(position: Int) = when (getItem(position)) {
AdapterItem.DeleteButton -> LoginExceptionsDeleteButtonViewHolder.LAYOUT_ID
AdapterItem.Header -> LoginExceptionsHeaderViewHolder.LAYOUT_ID
is AdapterItem.Item -> LoginExceptionsListItemViewHolder.LAYOUT_ID
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return when (viewType) {
LoginExceptionsDeleteButtonViewHolder.LAYOUT_ID -> LoginExceptionsDeleteButtonViewHolder(
view,
interactor
)
LoginExceptionsHeaderViewHolder.LAYOUT_ID -> LoginExceptionsHeaderViewHolder(view)
LoginExceptionsListItemViewHolder.LAYOUT_ID -> LoginExceptionsListItemViewHolder(
view,
interactor
)
else -> throw IllegalStateException()
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is LoginExceptionsListItemViewHolder) {
val adapterItem = getItem(position) as AdapterItem.Item
holder.bind(adapterItem.item)
}
}
sealed class AdapterItem {
object DeleteButton : AdapterItem()
object Header : AdapterItem()
data class Item(val item: LoginException) : AdapterItem()
}
internal object DiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
override fun areItemsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
when (oldItem) {
AdapterItem.DeleteButton, AdapterItem.Header -> oldItem === newItem
is AdapterItem.Item -> newItem is AdapterItem.Item && oldItem.item.id == newItem.item.id
}
@Suppress("DiffUtilEquals")
override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
oldItem == newItem
}
}

View File

@ -1,24 +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.loginexceptions
import mozilla.components.feature.logins.exceptions.LoginException
/**
* Interactor for the exceptions screen
* Provides implementations for the ExceptionsViewInteractor
*/
class LoginExceptionsInteractor(
private val deleteOne: (LoginException) -> Unit,
private val deleteAll: () -> Unit
) : ExceptionsViewInteractor {
override fun onDeleteAll() {
deleteAll.invoke()
}
override fun onDeleteOne(item: LoginException) {
deleteOne.invoke(item)
}
}

View File

@ -1,62 +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.loginexceptions
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.component_exceptions.*
import mozilla.components.feature.logins.exceptions.LoginException
import org.mozilla.fenix.R
/**
* Interface for the ExceptionsViewInteractor. This interface is implemented by objects that want
* to respond to user interaction on the ExceptionsView
*/
interface ExceptionsViewInteractor {
/**
* Called whenever all exception items are deleted
*/
fun onDeleteAll()
/**
* Called whenever one exception item is deleted
*/
fun onDeleteOne(item: LoginException)
}
/**
* View that contains and configures the Exceptions List
*/
class LoginExceptionsView(
container: ViewGroup,
val interactor: LoginExceptionsInteractor
) : LayoutContainer {
override val containerView: FrameLayout = LayoutInflater.from(container.context)
.inflate(R.layout.component_exceptions, container, true)
.findViewById(R.id.exceptions_wrapper)
private val exceptionsAdapter = LoginExceptionsAdapter(interactor)
init {
exceptions_learn_more.isVisible = false
exceptions_empty_message.text =
containerView.context.getString(R.string.preferences_passwords_exceptions_description_empty)
exceptions_list.apply {
adapter = exceptionsAdapter
layoutManager = LinearLayoutManager(containerView.context)
}
}
fun update(state: ExceptionsFragmentState) {
exceptions_empty_view.isVisible = state.items.isEmpty()
exceptions_list.isVisible = state.items.isNotEmpty()
exceptionsAdapter.updateData(state.items)
}
}

View File

@ -1,28 +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.loginexceptions.viewholders
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.delete_exceptions_button.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.loginexceptions.LoginExceptionsInteractor
class LoginExceptionsDeleteButtonViewHolder(
view: View,
private val interactor: LoginExceptionsInteractor
) : RecyclerView.ViewHolder(view) {
private val deleteButton = view.removeAllExceptions
init {
deleteButton.setOnClickListener {
interactor.onDeleteAll()
}
}
companion object {
const val LAYOUT_ID = R.layout.delete_logins_exceptions_button
}
}

View File

@ -1,50 +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.loginexceptions.viewholders
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.exception_item.view.*
import mozilla.components.feature.logins.exceptions.LoginException
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.loadIntoView
import org.mozilla.fenix.loginexceptions.LoginExceptionsInteractor
/**
* View holder for a single website that is exempted from Tracking Protection.
*/
class LoginExceptionsListItemViewHolder(
view: View,
private val interactor: LoginExceptionsInteractor
) : RecyclerView.ViewHolder(view) {
private val favicon = view.favicon_image
private val url = view.webAddressView
private val deleteButton = view.delete_exception
private var item: LoginException? = null
init {
deleteButton.setOnClickListener {
item?.let {
interactor.onDeleteOne(it)
}
}
}
fun bind(item: LoginException) {
this.item = item
url.text = item.origin
}
private fun updateFavIcon(url: String) {
favicon.context.components.core.icons.loadIntoView(favicon, url)
}
companion object {
const val LAYOUT_ID = R.layout.exception_item
}
}

View File

@ -1,78 +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.trackingprotectionexceptions
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
import org.mozilla.fenix.trackingprotectionexceptions.viewholders.ExceptionsDeleteButtonViewHolder
import org.mozilla.fenix.trackingprotectionexceptions.viewholders.ExceptionsHeaderViewHolder
import org.mozilla.fenix.trackingprotectionexceptions.viewholders.ExceptionsListItemViewHolder
/**
* Adapter for a list of sites that are exempted from Tracking Protection,
* along with controls to remove the exception.
*/
class ExceptionsAdapter(
private val interactor: ExceptionsInteractor
) : ListAdapter<ExceptionsAdapter.AdapterItem, RecyclerView.ViewHolder>(DiffCallback) {
/**
* Change the list of items that are displayed.
* Header and footer items are added to the list as well.
*/
fun updateData(exceptions: List<TrackingProtectionException>) {
val adapterItems = mutableListOf<AdapterItem>()
adapterItems.add(AdapterItem.Header)
exceptions.mapTo(adapterItems) { AdapterItem.Item(it) }
adapterItems.add(AdapterItem.DeleteButton)
submitList(adapterItems)
}
override fun getItemViewType(position: Int) = when (getItem(position)) {
AdapterItem.DeleteButton -> ExceptionsDeleteButtonViewHolder.LAYOUT_ID
AdapterItem.Header -> ExceptionsHeaderViewHolder.LAYOUT_ID
is AdapterItem.Item -> ExceptionsListItemViewHolder.LAYOUT_ID
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return when (viewType) {
ExceptionsDeleteButtonViewHolder.LAYOUT_ID -> ExceptionsDeleteButtonViewHolder(
view,
interactor
)
ExceptionsHeaderViewHolder.LAYOUT_ID -> ExceptionsHeaderViewHolder(view)
ExceptionsListItemViewHolder.LAYOUT_ID -> ExceptionsListItemViewHolder(view, interactor)
else -> throw IllegalStateException()
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is ExceptionsListItemViewHolder) {
val adapterItem = getItem(position) as AdapterItem.Item
holder.bind(adapterItem.item)
}
}
sealed class AdapterItem {
object DeleteButton : AdapterItem()
object Header : AdapterItem()
data class Item(val item: TrackingProtectionException) : AdapterItem()
}
private object DiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
override fun areItemsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
areContentsTheSame(oldItem, newItem)
@Suppress("DiffUtilEquals")
override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
oldItem == newItem
}
}

View File

@ -1,29 +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.trackingprotectionexceptions
import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
/**
* Interactor for the exceptions screen
* Provides implementations for the ExceptionsViewInteractor
*/
class ExceptionsInteractor(
private val learnMore: () -> Unit,
private val deleteOne: (TrackingProtectionException) -> Unit,
private val deleteAll: () -> Unit
) : ExceptionsViewInteractor {
override fun onLearnMore() {
learnMore.invoke()
}
override fun onDeleteAll() {
deleteAll.invoke()
}
override fun onDeleteOne(item: TrackingProtectionException) {
deleteOne.invoke(item)
}
}

View File

@ -1,76 +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.trackingprotectionexceptions
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.component_exceptions.*
import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.addUnderline
/**
* Interface for the ExceptionsViewInteractor. This interface is implemented by objects that want
* to respond to user interaction on the ExceptionsView
*/
interface ExceptionsViewInteractor {
/**
* Called whenever learn more about tracking protection is tapped
*/
fun onLearnMore()
/**
* Called whenever all exception items are deleted
*/
fun onDeleteAll()
/**
* Called whenever one exception item is deleted
*/
fun onDeleteOne(item: TrackingProtectionException)
}
/**
* View that contains and configures the Exceptions List
*/
class ExceptionsView(
container: ViewGroup,
interactor: ExceptionsInteractor
) : LayoutContainer {
override val containerView: FrameLayout = LayoutInflater.from(container.context)
.inflate(R.layout.component_exceptions, container, true)
.findViewById(R.id.exceptions_wrapper)
private val exceptionsAdapter =
ExceptionsAdapter(
interactor
)
init {
exceptions_list.apply {
adapter = exceptionsAdapter
layoutManager = LinearLayoutManager(container.context)
}
with(exceptions_learn_more) {
addUnderline()
movementMethod = LinkMovementMethod.getInstance()
setOnClickListener { interactor.onLearnMore() }
}
}
fun update(state: ExceptionsFragmentState) {
exceptions_empty_view.isVisible = state.items.isEmpty()
exceptions_list.isVisible = state.items.isNotEmpty()
exceptionsAdapter.updateData(state.items)
}
}

View File

@ -1,107 +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.trackingprotectionexceptions
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import kotlinx.android.synthetic.main.fragment_exceptions.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
import mozilla.components.feature.session.TrackingProtectionUseCases
import mozilla.components.lib.state.ext.consumeFrom
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.settings.SupportUtils
/**
* Displays a list of sites that are exempted from Tracking Protection,
* along with controls to remove the exception.
*/
class TrackingProtectionExceptionsFragment : Fragment() {
private lateinit var exceptionsStore: ExceptionsFragmentStore
private lateinit var exceptionsView: ExceptionsView
private lateinit var exceptionsInteractor: ExceptionsInteractor
private lateinit var trackingProtectionUseCases: TrackingProtectionUseCases
override fun onResume() {
super.onResume()
showToolbar(getString(R.string.preference_exceptions))
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_exceptions, container, false)
trackingProtectionUseCases = view.context.components.useCases.trackingProtectionUseCases
exceptionsStore = StoreProvider.get(this) {
ExceptionsFragmentStore(
ExceptionsFragmentState(
items = emptyList()
)
)
}
exceptionsInteractor =
ExceptionsInteractor(
::openLearnMore,
::deleteOneItem,
::deleteAllItems
)
exceptionsView =
ExceptionsView(
view.exceptionsLayout,
exceptionsInteractor
)
reloadExceptions()
return view
}
@ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
consumeFrom(exceptionsStore) {
exceptionsView.update(it)
}
}
private fun deleteAllItems() {
trackingProtectionUseCases.removeAllExceptions()
reloadExceptions()
}
private fun deleteOneItem(item: TrackingProtectionException) {
trackingProtectionUseCases.removeException(item)
Log.e("Remove one exception", "$item")
reloadExceptions()
}
private fun openLearnMore() {
(activity as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = SupportUtils.getGenericSumoURLForTopic
(SupportUtils.SumoTopic.TRACKING_PROTECTION),
newTab = true,
from = BrowserDirection.FromTrackingProtectionExceptions
)
}
private fun reloadExceptions() {
trackingProtectionUseCases.fetchExceptions { resultList ->
exceptionsStore.dispatch(
ExceptionsFragmentAction.Change(
resultList
)
)
}
}
}

View File

@ -1,17 +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.trackingprotectionexceptions.viewholders
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import org.mozilla.fenix.R
class ExceptionsHeaderViewHolder(
view: View
) : RecyclerView.ViewHolder(view) {
companion object {
const val LAYOUT_ID = R.layout.exceptions_description
}
}

View File

@ -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.trackingprotectionexceptions.viewholders
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.exception_item.view.*
import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
import org.mozilla.fenix.R
import org.mozilla.fenix.trackingprotectionexceptions.ExceptionsInteractor
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.loadIntoView
/**
* View holder for a single website that is exempted from Tracking Protection.
*/
class ExceptionsListItemViewHolder(
view: View,
private val interactor: ExceptionsInteractor
) : RecyclerView.ViewHolder(view) {
private val favicon = view.favicon_image
private val url = view.webAddressView
private val deleteButton = view.delete_exception
private var item: TrackingProtectionException? = null
init {
deleteButton.setOnClickListener {
item?.let {
interactor.onDeleteOne(it)
}
}
}
fun bind(item: TrackingProtectionException) {
this.item = item
url.text = item.url
updateFavIcon(item.url)
}
private fun updateFavIcon(url: String) {
favicon.context.components.core.icons.loadIntoView(favicon, url)
}
companion object {
const val LAYOUT_ID = R.layout.exception_item
}
}

View File

@ -45,5 +45,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
tools:listitem="@layout/exception_item" />
tools:listheader="@layout/exceptions_description"
tools:listitem="@layout/exception_item"
tools:listfooter="@layout/delete_exceptions_button" />
</FrameLayout>

View File

@ -2,7 +2,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/. -->
<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
<com.google.android.material.button.MaterialButton
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/removeAllExceptions"
style="@style/DestructiveButton"
android:layout_marginHorizontal="16dp"

View File

@ -3,10 +3,11 @@
- 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/. -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/exceptions_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:text="@string/enhanced_tracking_protection_exceptions"
tools:text="@string/enhanced_tracking_protection_exceptions"
android:textColor="?primaryText"
android:textSize="16sp" />

View File

@ -347,7 +347,7 @@
<fragment
android:id="@+id/loginExceptionsFragment"
android:name="org.mozilla.fenix.loginexceptions.LoginExceptionsFragment"
android:name="org.mozilla.fenix.exceptions.login.LoginExceptionsFragment"
android:label="@string/preferences_passwords_exceptions"
tools:layout="@layout/fragment_exceptions" />
@ -609,7 +609,7 @@
android:label="@string/preferences_delete_browsing_data" />
<fragment
android:id="@+id/trackingProtectionExceptionsFragment"
android:name="org.mozilla.fenix.trackingprotectionexceptions.TrackingProtectionExceptionsFragment"
android:name="org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExceptionsFragment"
android:label="@string/preference_exceptions" />
<dialog
android:id="@+id/collectionCreationFragment"

View File

@ -0,0 +1,26 @@
/* 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.exceptions.login
import io.mockk.mockk
import mozilla.components.feature.logins.exceptions.LoginException
import mozilla.components.support.test.ext.joinBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotSame
import org.junit.Test
class LoginExceptionFragmentStoreTest {
@Test
fun onChange() {
val initialState = ExceptionsFragmentState()
val store = ExceptionsFragmentStore(initialState)
val newExceptionsItem: LoginException = mockk()
store.dispatch(ExceptionsFragmentAction.Change(listOf(newExceptionsItem))).joinBlocking()
assertNotSame(initialState, store.state)
assertEquals(listOf(newExceptionsItem), store.state.items)
}
}

View File

@ -0,0 +1,149 @@
/* 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.exceptions.login
import android.content.Context
import android.widget.FrameLayout
import androidx.appcompat.view.ContextThemeWrapper
import io.mockk.every
import io.mockk.mockk
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.R
import org.mozilla.fenix.exceptions.ExceptionsAdapter
import org.mozilla.fenix.exceptions.viewholders.ExceptionsDeleteButtonViewHolder
import org.mozilla.fenix.exceptions.viewholders.ExceptionsHeaderViewHolder
import org.mozilla.fenix.exceptions.viewholders.ExceptionsListItemViewHolder
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)
class LoginExceptionsAdapterTest {
private lateinit var interactor: LoginExceptionsInteractor
private lateinit var adapter: LoginExceptionsAdapter
private lateinit var context: Context
@Before
fun setup() {
interactor = mockk()
adapter = LoginExceptionsAdapter(interactor)
context = ContextThemeWrapper(testContext, R.style.NormalTheme)
}
@Test
fun `creates correct view holder type`() {
val parent = FrameLayout(context)
adapter.updateData(listOf(mockk(), mockk()))
assertEquals(4, adapter.itemCount)
val holders = (0 until adapter.itemCount).asSequence()
.map { i -> adapter.getItemViewType(i) }
.map { viewType -> adapter.onCreateViewHolder(parent, viewType) }
.toList()
assertEquals(4, holders.size)
assertTrue(holders[0] is ExceptionsHeaderViewHolder)
assertTrue(holders[1] is ExceptionsListItemViewHolder<*>)
assertTrue(holders[2] is ExceptionsListItemViewHolder<*>)
assertTrue(holders[3] is ExceptionsDeleteButtonViewHolder)
}
@Test
fun `headers and delete should check if the other object is the same`() {
assertTrue(
LoginExceptionsAdapter.DiffCallback.areItemsTheSame(
ExceptionsAdapter.AdapterItem.Header,
ExceptionsAdapter.AdapterItem.Header
)
)
assertTrue(
LoginExceptionsAdapter.DiffCallback.areItemsTheSame(
ExceptionsAdapter.AdapterItem.DeleteButton,
ExceptionsAdapter.AdapterItem.DeleteButton
)
)
assertFalse(
LoginExceptionsAdapter.DiffCallback.areItemsTheSame(
ExceptionsAdapter.AdapterItem.Header,
ExceptionsAdapter.AdapterItem.DeleteButton
)
)
assertTrue(
LoginExceptionsAdapter.DiffCallback.areContentsTheSame(
ExceptionsAdapter.AdapterItem.Header,
ExceptionsAdapter.AdapterItem.Header
)
)
assertTrue(
LoginExceptionsAdapter.DiffCallback.areContentsTheSame(
ExceptionsAdapter.AdapterItem.DeleteButton,
ExceptionsAdapter.AdapterItem.DeleteButton
)
)
assertFalse(
LoginExceptionsAdapter.DiffCallback.areContentsTheSame(
ExceptionsAdapter.AdapterItem.DeleteButton,
ExceptionsAdapter.AdapterItem.Header
)
)
}
@Test
fun `items with the same id should be marked as same`() {
assertTrue(
LoginExceptionsAdapter.DiffCallback.areItemsTheSame(
LoginExceptionsAdapter.LoginAdapterItem(
mockk {
every { id } returns 12L
}
),
LoginExceptionsAdapter.LoginAdapterItem(
mockk {
every { id } returns 12L
}
)
)
)
assertFalse(
LoginExceptionsAdapter.DiffCallback.areItemsTheSame(
LoginExceptionsAdapter.LoginAdapterItem(
mockk {
every { id } returns 14L
}
),
LoginExceptionsAdapter.LoginAdapterItem(
mockk {
every { id } returns 12L
}
)
)
)
assertFalse(
LoginExceptionsAdapter.DiffCallback.areItemsTheSame(
LoginExceptionsAdapter.LoginAdapterItem(
mockk {
every { id } returns 14L
}
),
ExceptionsAdapter.AdapterItem.Header
)
)
assertFalse(
LoginExceptionsAdapter.DiffCallback.areItemsTheSame(
ExceptionsAdapter.AdapterItem.DeleteButton,
LoginExceptionsAdapter.LoginAdapterItem(
mockk {
every { id } returns 14L
}
)
)
)
}
}

View File

@ -0,0 +1,42 @@
/* 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.exceptions.login
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.feature.logins.exceptions.LoginException
import mozilla.components.feature.logins.exceptions.LoginExceptionStorage
import org.junit.Before
import org.junit.Test
@ExperimentalCoroutinesApi
class LoginExceptionsInteractorTest {
private lateinit var loginExceptionStorage: LoginExceptionStorage
private lateinit var interactor: LoginExceptionsInteractor
private val scope = TestCoroutineScope()
@Before
fun setup() {
loginExceptionStorage = mockk(relaxed = true)
interactor = DefaultLoginExceptionsInteractor(scope, loginExceptionStorage)
}
@Test
fun onDeleteAll() = scope.runBlockingTest {
interactor.onDeleteAll()
verify { loginExceptionStorage.deleteAllLoginExceptions() }
}
@Test
fun onDeleteOne() = scope.runBlockingTest {
val exceptionsItem: LoginException = mockk()
interactor.onDeleteOne(exceptionsItem)
verify { loginExceptionStorage.removeLoginException(exceptionsItem) }
}
}

View File

@ -2,7 +2,7 @@
* 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.loginexceptions
package org.mozilla.fenix.exceptions.login
import android.view.ViewGroup
import android.widget.FrameLayout
@ -30,7 +30,10 @@ class LoginExceptionsViewTest {
fun setup() {
parent = FrameLayout(testContext)
interactor = mockk()
view = LoginExceptionsView(parent, interactor)
view = LoginExceptionsView(
parent,
interactor
)
}
@Test
@ -45,9 +48,7 @@ class LoginExceptionsViewTest {
@Test
fun `hide list when there are no items`() {
view.update(ExceptionsFragmentState(
items = emptyList()
))
view.update(emptyList())
assertTrue(view.exceptions_empty_view.isVisible)
assertFalse(view.exceptions_list.isVisible)
@ -55,9 +56,7 @@ class LoginExceptionsViewTest {
@Test
fun `shows list when there are items`() {
view.update(ExceptionsFragmentState(
items = listOf(mockk())
))
view.update(listOf(mockk()))
assertFalse(view.exceptions_empty_view.isVisible)
assertTrue(view.exceptions_list.isVisible)

View File

@ -0,0 +1,151 @@
/* 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.exceptions.trackingprotection
import android.content.Context
import android.widget.FrameLayout
import androidx.appcompat.view.ContextThemeWrapper
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.R
import org.mozilla.fenix.exceptions.ExceptionsAdapter
import org.mozilla.fenix.exceptions.viewholders.ExceptionsDeleteButtonViewHolder
import org.mozilla.fenix.exceptions.viewholders.ExceptionsHeaderViewHolder
import org.mozilla.fenix.exceptions.viewholders.ExceptionsListItemViewHolder
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@ExperimentalCoroutinesApi
@RunWith(FenixRobolectricTestRunner::class)
class TrackingProtectionExceptionsAdapterTest {
private lateinit var interactor: TrackingProtectionExceptionsInteractor
private lateinit var adapter: TrackingProtectionExceptionsAdapter
private lateinit var context: Context
@Before
fun setup() {
interactor = mockk()
adapter = TrackingProtectionExceptionsAdapter(interactor)
context = ContextThemeWrapper(testContext, R.style.NormalTheme)
}
@Test
fun `creates correct view holder type`() {
val parent = FrameLayout(context)
adapter.updateData(listOf(mockk(), mockk()))
assertEquals(4, adapter.itemCount)
val holders = (0 until adapter.itemCount).asSequence()
.map { i -> adapter.getItemViewType(i) }
.map { viewType -> adapter.onCreateViewHolder(parent, viewType) }
.toList()
assertEquals(4, holders.size)
assertTrue(holders[0] is ExceptionsHeaderViewHolder)
assertTrue(holders[1] is ExceptionsListItemViewHolder<*>)
assertTrue(holders[2] is ExceptionsListItemViewHolder<*>)
assertTrue(holders[3] is ExceptionsDeleteButtonViewHolder)
}
@Test
fun `headers and delete should check if the other object is the same`() {
assertTrue(
TrackingProtectionExceptionsAdapter.DiffCallback.areItemsTheSame(
ExceptionsAdapter.AdapterItem.Header,
ExceptionsAdapter.AdapterItem.Header
)
)
assertTrue(
TrackingProtectionExceptionsAdapter.DiffCallback.areItemsTheSame(
ExceptionsAdapter.AdapterItem.DeleteButton,
ExceptionsAdapter.AdapterItem.DeleteButton
)
)
assertFalse(
TrackingProtectionExceptionsAdapter.DiffCallback.areItemsTheSame(
ExceptionsAdapter.AdapterItem.Header,
ExceptionsAdapter.AdapterItem.DeleteButton
)
)
assertTrue(
TrackingProtectionExceptionsAdapter.DiffCallback.areContentsTheSame(
ExceptionsAdapter.AdapterItem.Header,
ExceptionsAdapter.AdapterItem.Header
)
)
assertTrue(
TrackingProtectionExceptionsAdapter.DiffCallback.areContentsTheSame(
ExceptionsAdapter.AdapterItem.DeleteButton,
ExceptionsAdapter.AdapterItem.DeleteButton
)
)
assertFalse(
TrackingProtectionExceptionsAdapter.DiffCallback.areContentsTheSame(
ExceptionsAdapter.AdapterItem.DeleteButton,
ExceptionsAdapter.AdapterItem.Header
)
)
}
@Test
fun `items with the same url should be marked as same`() {
assertTrue(
TrackingProtectionExceptionsAdapter.DiffCallback.areItemsTheSame(
TrackingProtectionExceptionsAdapter.TrackingProtectionAdapterItem(
mockk {
every { url } returns "https://mozilla.org"
}
),
TrackingProtectionExceptionsAdapter.TrackingProtectionAdapterItem(
mockk {
every { url } returns "https://mozilla.org"
}
)
)
)
assertFalse(
TrackingProtectionExceptionsAdapter.DiffCallback.areItemsTheSame(
TrackingProtectionExceptionsAdapter.TrackingProtectionAdapterItem(
mockk {
every { url } returns "https://mozilla.org"
}
),
TrackingProtectionExceptionsAdapter.TrackingProtectionAdapterItem(
mockk {
every { url } returns "https://firefox.com"
}
)
)
)
assertFalse(
TrackingProtectionExceptionsAdapter.DiffCallback.areItemsTheSame(
TrackingProtectionExceptionsAdapter.TrackingProtectionAdapterItem(
mockk {
every { url } returns "https://mozilla.org"
}
),
ExceptionsAdapter.AdapterItem.Header
)
)
assertFalse(
TrackingProtectionExceptionsAdapter.DiffCallback.areItemsTheSame(
ExceptionsAdapter.AdapterItem.DeleteButton,
TrackingProtectionExceptionsAdapter.TrackingProtectionAdapterItem(
mockk {
every { url } returns "https://mozilla.org"
}
)
)
)
}
}

View File

@ -2,21 +2,20 @@
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.loginexceptions
package org.mozilla.fenix.exceptions.trackingprotection
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import mozilla.components.feature.logins.exceptions.LoginException
import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotSame
import org.junit.Test
class LoginExceptionFragmentStoreTest {
class TrackingProtectionExceptionsFragmentStoreTest {
@Test
fun onChange() = runBlocking {
val initialState = emptyDefaultState()
val initialState = ExceptionsFragmentState()
val store = ExceptionsFragmentStore(initialState)
val newExceptionsItem: LoginException = mockk()
val newExceptionsItem = ExceptionItem("URL")
store.dispatch(ExceptionsFragmentAction.Change(listOf(newExceptionsItem))).join()
assertNotSame(initialState, store.state)
@ -26,7 +25,5 @@ class LoginExceptionFragmentStoreTest {
)
}
private fun emptyDefaultState(): ExceptionsFragmentState = ExceptionsFragmentState(
items = listOf()
)
private data class ExceptionItem(override val url: String) : TrackingProtectionException
}

View File

@ -0,0 +1,88 @@
/* 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.exceptions.trackingprotection
import io.mockk.CapturingSlot
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.slot
import io.mockk.verify
import io.mockk.verifySequence
import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
import mozilla.components.feature.session.TrackingProtectionUseCases
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.settings.SupportUtils
class TrackingProtectionExceptionsInteractorTest {
@MockK(relaxed = true) private lateinit var activity: HomeActivity
@MockK(relaxed = true) private lateinit var exceptionsStore: ExceptionsFragmentStore
@MockK(relaxed = true) private lateinit var trackingProtectionUseCases: TrackingProtectionUseCases
private lateinit var interactor: TrackingProtectionExceptionsInteractor
private lateinit var onResult: CapturingSlot<(List<TrackingProtectionException>) -> Unit>
@Before
fun setup() {
MockKAnnotations.init(this)
interactor = DefaultTrackingProtectionExceptionsInteractor(
activity = activity,
exceptionsStore = exceptionsStore,
trackingProtectionUseCases = trackingProtectionUseCases
)
onResult = slot()
every { trackingProtectionUseCases.fetchExceptions(capture(onResult)) } just Runs
}
@Test
fun onLearnMore() {
interactor.onLearnMore()
val supportUrl = SupportUtils.getGenericSumoURLForTopic(
SupportUtils.SumoTopic.TRACKING_PROTECTION
)
verify {
activity.openToBrowserAndLoad(
searchTermOrURL = supportUrl,
newTab = true,
from = BrowserDirection.FromTrackingProtectionExceptions
)
}
}
@Test
fun onDeleteAll() {
interactor.onDeleteAll()
verifySequence {
trackingProtectionUseCases.removeAllExceptions()
trackingProtectionUseCases.fetchExceptions(any())
}
val results = mockk<List<TrackingProtectionException>>()
onResult.captured(results)
verify { exceptionsStore.dispatch(ExceptionsFragmentAction.Change(results)) }
}
@Test
fun onDeleteOne() {
val exceptionsItem = mockk<TrackingProtectionException>()
interactor.onDeleteOne(exceptionsItem)
verifySequence {
trackingProtectionUseCases.removeException(exceptionsItem)
trackingProtectionUseCases.fetchExceptions(any())
}
val results = mockk<List<TrackingProtectionException>>()
onResult.captured(results)
verify { exceptionsStore.dispatch(ExceptionsFragmentAction.Change(results)) }
}
}

View File

@ -2,7 +2,7 @@
* 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.trackingprotectionexceptions
package org.mozilla.fenix.exceptions.trackingprotection
import android.text.Spannable
import android.text.method.LinkMovementMethod
@ -29,25 +29,28 @@ import org.junit.runner.RunWith
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)
class ExceptionsViewTest {
class TrackingProtectionExceptionsViewTest {
private lateinit var container: ViewGroup
private lateinit var interactor: ExceptionsInteractor
private lateinit var exceptionsView: ExceptionsView
private lateinit var interactor: TrackingProtectionExceptionsInteractor
private lateinit var exceptionsView: TrackingProtectionExceptionsView
@Before
fun setup() {
mockkConstructor(ExceptionsAdapter::class)
mockkConstructor(TrackingProtectionExceptionsAdapter::class)
container = FrameLayout(testContext)
interactor = mockk()
exceptionsView = ExceptionsView(container, interactor)
every { anyConstructed<ExceptionsAdapter>().updateData(any()) } just Runs
exceptionsView = TrackingProtectionExceptionsView(
container,
interactor
)
every { anyConstructed<TrackingProtectionExceptionsAdapter>().updateData(any()) } just Runs
}
@After
fun teardown() {
unmockkConstructor(ExceptionsAdapter::class)
unmockkConstructor(TrackingProtectionExceptionsAdapter::class)
}
@Test
@ -63,20 +66,20 @@ class ExceptionsViewTest {
@Test
fun `binds empty list to adapter`() {
exceptionsView.update(ExceptionsFragmentState(emptyList()))
exceptionsView.update(emptyList())
assertTrue(exceptionsView.exceptions_empty_view.isVisible)
assertFalse(exceptionsView.exceptions_list.isVisible)
verify { anyConstructed<ExceptionsAdapter>().updateData(emptyList()) }
verify { anyConstructed<TrackingProtectionExceptionsAdapter>().updateData(emptyList()) }
}
@Test
fun `binds list with items to adapter`() {
val items = listOf<TrackingProtectionException>(mockk(), mockk())
exceptionsView.update(ExceptionsFragmentState(items))
exceptionsView.update(items)
assertFalse(exceptionsView.exceptions_empty_view.isVisible)
assertTrue(exceptionsView.exceptions_list.isVisible)
verify { anyConstructed<ExceptionsAdapter>().updateData(items) }
verify { anyConstructed<TrackingProtectionExceptionsAdapter>().updateData(items) }
}
}

View File

@ -2,12 +2,14 @@
* 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.loginexceptions.viewholders
package org.mozilla.fenix.exceptions.viewholders
import android.view.View
import com.google.android.material.button.MaterialButton
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.slot
@ -15,28 +17,25 @@ import io.mockk.verify
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.loginexceptions.LoginExceptionsInteractor
import org.mozilla.fenix.exceptions.ExceptionsInteractor
class LoginExceptionsDeleteButtonViewHolderTest {
class ExceptionsDeleteButtonViewHolderTest {
private lateinit var view: View
private lateinit var deleteButton: MaterialButton
private lateinit var interactor: LoginExceptionsInteractor
@MockK private lateinit var view: View
@MockK private lateinit var deleteButton: MaterialButton
@MockK private lateinit var interactor: ExceptionsInteractor<Unit>
@Before
fun setup() {
deleteButton = mockk()
view = mockk {
every { findViewById<MaterialButton>(R.id.removeAllExceptions) } returns deleteButton
}
interactor = mockk()
MockKAnnotations.init(this)
every { view.findViewById<MaterialButton>(R.id.removeAllExceptions) } returns deleteButton
}
@Test
fun `delete button calls interactor`() {
val slot = slot<View.OnClickListener>()
every { deleteButton.setOnClickListener(capture(slot)) } just Runs
LoginExceptionsDeleteButtonViewHolder(view, interactor)
ExceptionsDeleteButtonViewHolder(view, interactor)
every { interactor.onDeleteAll() } just Runs
slot.captured.onClick(mockk())

View File

@ -2,7 +2,7 @@
* 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.loginexceptions.viewholders
package org.mozilla.fenix.exceptions.viewholders
import android.view.View
import android.widget.TextView
@ -13,7 +13,7 @@ import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.R
class LoginExceptionsHeaderViewHolderTest {
class ExceptionsHeaderViewHolderTest {
private lateinit var view: View
private lateinit var description: TextView
@ -31,7 +31,7 @@ class LoginExceptionsHeaderViewHolderTest {
@Test
fun `sets description text`() {
LoginExceptionsHeaderViewHolder(view)
ExceptionsHeaderViewHolder(view, R.string.preferences_passwords_exceptions_description)
verify { description.text = "Logins and passwords will not be saved for these sites." }
}
}

View File

@ -0,0 +1,74 @@
/* 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.exceptions.viewholders
import android.view.View
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
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.slot
import io.mockk.verify
import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.browser.icons.IconRequest
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.exceptions.ExceptionsInteractor
class ExceptionsListItemViewHolderTest {
@MockK private lateinit var view: View
@MockK(relaxUnitFun = true) private lateinit var url: TextView
@MockK(relaxUnitFun = true) private lateinit var deleteButton: ImageButton
@MockK private lateinit var favicon: ImageView
@MockK private lateinit var icons: BrowserIcons
@MockK private lateinit var interactor: ExceptionsInteractor<Exception>
@Before
fun setup() {
MockKAnnotations.init(this)
every { view.findViewById<TextView>(R.id.webAddressView) } returns url
every { view.findViewById<ImageButton>(R.id.delete_exception) } returns deleteButton
every { view.findViewById<ImageView>(R.id.favicon_image) } returns favicon
every { icons.loadIntoView(favicon, any()) } returns mockk()
}
@Test
fun `sets url text and loads favicon - mozilla`() {
ExceptionsListItemViewHolder(view, interactor, icons)
.bind(Exception(), url = "mozilla.org")
verify { url.text = "mozilla.org" }
verify { icons.loadIntoView(favicon, IconRequest("mozilla.org")) }
}
@Test
fun `sets url text and loads favicon - example`() {
ExceptionsListItemViewHolder(view, interactor, icons)
.bind(Exception(), url = "https://example.com/icon.svg")
verify { url.text = "https://example.com/icon.svg" }
verify { icons.loadIntoView(favicon, IconRequest("https://example.com/icon.svg")) }
}
@Test
fun `delete button calls interactor`() {
val slot = slot<View.OnClickListener>()
val exception = Exception()
every { deleteButton.setOnClickListener(capture(slot)) } just Runs
ExceptionsListItemViewHolder(view, interactor, icons).bind(exception, url = "mozilla.org")
every { interactor.onDeleteOne(exception) } just Runs
slot.captured.onClick(mockk())
verify { interactor.onDeleteOne(exception) }
}
class Exception
}

View File

@ -1,113 +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.loginexceptions
import android.widget.LinearLayout
import androidx.appcompat.view.ContextThemeWrapper
import io.mockk.every
import io.mockk.mockk
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
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.loginexceptions.viewholders.LoginExceptionsDeleteButtonViewHolder
import org.mozilla.fenix.loginexceptions.viewholders.LoginExceptionsHeaderViewHolder
import org.mozilla.fenix.loginexceptions.viewholders.LoginExceptionsListItemViewHolder
@RunWith(FenixRobolectricTestRunner::class)
class LoginExceptionsAdapterTest {
private lateinit var interactor: LoginExceptionsInteractor
private lateinit var adapter: LoginExceptionsAdapter
@Before
fun setup() {
interactor = mockk()
adapter = LoginExceptionsAdapter(interactor)
}
@Test
fun `creates correct view holder type`() {
val parent = LinearLayout(ContextThemeWrapper(testContext, R.style.NormalTheme))
adapter.updateData(listOf(mockk(), mockk()))
assertEquals(4, adapter.itemCount)
val holders = (0 until adapter.itemCount).asSequence()
.map { i -> adapter.getItemViewType(i) }
.map { viewType -> adapter.onCreateViewHolder(parent, viewType) }
.toList()
assertEquals(4, holders.size)
assertTrue(holders[0] is LoginExceptionsHeaderViewHolder)
assertTrue(holders[1] is LoginExceptionsListItemViewHolder)
assertTrue(holders[2] is LoginExceptionsListItemViewHolder)
assertTrue(holders[3] is LoginExceptionsDeleteButtonViewHolder)
}
@Test
fun `headers and delete should check if the other object is the same`() {
assertTrue(LoginExceptionsAdapter.DiffCallback.areItemsTheSame(
LoginExceptionsAdapter.AdapterItem.Header,
LoginExceptionsAdapter.AdapterItem.Header
))
assertTrue(LoginExceptionsAdapter.DiffCallback.areItemsTheSame(
LoginExceptionsAdapter.AdapterItem.DeleteButton,
LoginExceptionsAdapter.AdapterItem.DeleteButton
))
assertFalse(LoginExceptionsAdapter.DiffCallback.areItemsTheSame(
LoginExceptionsAdapter.AdapterItem.Header,
LoginExceptionsAdapter.AdapterItem.DeleteButton
))
assertTrue(LoginExceptionsAdapter.DiffCallback.areContentsTheSame(
LoginExceptionsAdapter.AdapterItem.Header,
LoginExceptionsAdapter.AdapterItem.Header
))
assertTrue(LoginExceptionsAdapter.DiffCallback.areContentsTheSame(
LoginExceptionsAdapter.AdapterItem.DeleteButton,
LoginExceptionsAdapter.AdapterItem.DeleteButton
))
assertFalse(LoginExceptionsAdapter.DiffCallback.areContentsTheSame(
LoginExceptionsAdapter.AdapterItem.DeleteButton,
LoginExceptionsAdapter.AdapterItem.Header
))
}
@Test
fun `items with the same id should be marked as same`() {
assertTrue(LoginExceptionsAdapter.DiffCallback.areItemsTheSame(
LoginExceptionsAdapter.AdapterItem.Item(mockk {
every { id } returns 12L
}),
LoginExceptionsAdapter.AdapterItem.Item(mockk {
every { id } returns 12L
})
))
assertFalse(LoginExceptionsAdapter.DiffCallback.areItemsTheSame(
LoginExceptionsAdapter.AdapterItem.Item(mockk {
every { id } returns 14L
}),
LoginExceptionsAdapter.AdapterItem.Item(mockk {
every { id } returns 12L
})
))
assertFalse(LoginExceptionsAdapter.DiffCallback.areItemsTheSame(
LoginExceptionsAdapter.AdapterItem.Item(mockk {
every { id } returns 14L
}),
LoginExceptionsAdapter.AdapterItem.Header
))
assertFalse(LoginExceptionsAdapter.DiffCallback.areItemsTheSame(
LoginExceptionsAdapter.AdapterItem.DeleteButton,
LoginExceptionsAdapter.AdapterItem.Item(mockk {
every { id } returns 14L
})
))
}
}

View File

@ -1,36 +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.loginexceptions
import io.mockk.mockk
import mozilla.components.feature.logins.exceptions.LoginException
import org.junit.Assert.assertEquals
import org.junit.Test
class LoginExceptionsInteractorTest {
@Test
fun onDeleteAll() {
var onDeleteAll = false
val interactor = LoginExceptionsInteractor(
mockk(),
{ onDeleteAll = true }
)
interactor.onDeleteAll()
assertEquals(true, onDeleteAll)
}
@Test
fun onDeleteOne() {
var exceptionsItemReceived: LoginException? = null
val exceptionsItem: LoginException = mockk()
val interactor = LoginExceptionsInteractor(
{ exceptionsItemReceived = exceptionsItem },
mockk()
)
interactor.onDeleteOne(exceptionsItem)
assertEquals(exceptionsItemReceived, exceptionsItem)
}
}

View File

@ -1,63 +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.loginexceptions.viewholders
import android.view.View
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import mozilla.components.feature.logins.exceptions.LoginException
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.loginexceptions.LoginExceptionsInteractor
class LoginExceptionsListItemViewHolderTest {
private lateinit var view: View
private lateinit var url: TextView
private lateinit var deleteButton: ImageButton
private lateinit var interactor: LoginExceptionsInteractor
@Before
fun setup() {
url = mockk(relaxUnitFun = true)
deleteButton = mockk(relaxUnitFun = true)
view = mockk {
every { findViewById<TextView>(R.id.webAddressView) } returns url
every { findViewById<ImageButton>(R.id.delete_exception) } returns deleteButton
every { findViewById<ImageView>(R.id.favicon_image) } returns mockk()
}
interactor = mockk()
}
@Test
fun `sets url text`() {
LoginExceptionsListItemViewHolder(view, interactor).bind(mockk {
every { origin } returns "mozilla.org"
})
verify { url.text = "mozilla.org" }
}
@Test
fun `delete button calls interactor`() {
val slot = slot<View.OnClickListener>()
val loginException = mockk<LoginException> {
every { origin } returns "mozilla.org"
}
every { deleteButton.setOnClickListener(capture(slot)) } just Runs
LoginExceptionsListItemViewHolder(view, interactor).bind(loginException)
every { interactor.onDeleteOne(loginException) } just Runs
slot.captured.onClick(mockk())
verify { interactor.onDeleteOne(loginException) }
}
}

View File

@ -1,42 +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.trackingprotectionexceptions
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.trackingprotectionexceptions.viewholders.ExceptionsDeleteButtonViewHolder
import org.mozilla.fenix.trackingprotectionexceptions.viewholders.ExceptionsHeaderViewHolder
import org.mozilla.fenix.trackingprotectionexceptions.viewholders.ExceptionsListItemViewHolder
@ExperimentalCoroutinesApi
@RunWith(FenixRobolectricTestRunner::class)
class ExceptionsAdapterTest {
private lateinit var interactor: ExceptionsInteractor
private lateinit var adapter: ExceptionsAdapter
@Before
fun setup() {
interactor = mockk()
adapter = ExceptionsAdapter(interactor)
}
@Test
fun `binds header and delete button with other adapter items`() = runBlockingTest {
adapter.updateData(listOf(mockk(), mockk()))
assertEquals(4, adapter.itemCount)
assertEquals(ExceptionsHeaderViewHolder.LAYOUT_ID, adapter.getItemViewType(0))
assertEquals(ExceptionsListItemViewHolder.LAYOUT_ID, adapter.getItemViewType(1))
assertEquals(ExceptionsListItemViewHolder.LAYOUT_ID, adapter.getItemViewType(2))
assertEquals(ExceptionsDeleteButtonViewHolder.LAYOUT_ID, adapter.getItemViewType(3))
}
}

View File

@ -1,54 +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.trackingprotectionexceptions
import io.mockk.mockk
import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
import org.junit.Assert.assertEquals
import org.junit.Test
class ExceptionsInteractorTest {
@Test
fun onLearnMore() {
var learnMoreClicked = false
val interactor =
ExceptionsInteractor(
{ learnMoreClicked = true },
mockk(),
mockk()
)
interactor.onLearnMore()
assertEquals(true, learnMoreClicked)
}
@Test
fun onDeleteAll() {
var onDeleteAll = false
val interactor =
ExceptionsInteractor(
mockk(),
mockk(),
{ onDeleteAll = true }
)
interactor.onDeleteAll()
assertEquals(true, onDeleteAll)
}
@Test
fun onDeleteOne() {
var exceptionsItemReceived: TrackingProtectionException? = null
val exceptionsItem =
ExceptionItem("url")
val interactor =
ExceptionsInteractor(
mockk(),
{ exceptionsItemReceived = exceptionsItem },
mockk()
)
interactor.onDeleteOne(exceptionsItem)
assertEquals(exceptionsItemReceived, exceptionsItem)
}
}

View File

@ -1,35 +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.trackingprotectionexceptions
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotSame
import org.junit.Test
class TrackingProtectionExceptionsFragmentStoreTest {
@Test
fun onChange() = runBlocking {
val initialState = emptyDefaultState()
val store =
ExceptionsFragmentStore(
initialState
)
val newExceptionsItem =
ExceptionItem("URL")
store.dispatch(ExceptionsFragmentAction.Change(listOf(newExceptionsItem))).join()
assertNotSame(initialState, store.state)
assertEquals(
store.state.items,
listOf(newExceptionsItem)
)
}
private fun emptyDefaultState(): ExceptionsFragmentState =
ExceptionsFragmentState(
items = listOf()
)
}

View File

@ -1,43 +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.trackingprotectionexceptions.viewholders
import android.view.LayoutInflater
import android.view.View
import androidx.appcompat.view.ContextThemeWrapper
import io.mockk.mockk
import io.mockk.verify
import kotlinx.android.synthetic.main.delete_exceptions_button.view.*
import mozilla.components.support.test.robolectric.testContext
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.R
import org.mozilla.fenix.trackingprotectionexceptions.ExceptionsInteractor
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)
class ExceptionsDeleteButtonViewHolderTest {
private lateinit var view: View
private lateinit var interactor: ExceptionsInteractor
private lateinit var viewHolder: ExceptionsDeleteButtonViewHolder
@Before
fun setup() {
val appCompatContext = ContextThemeWrapper(testContext, R.style.NormalTheme)
view = LayoutInflater.from(appCompatContext)
.inflate(ExceptionsDeleteButtonViewHolder.LAYOUT_ID, null)
interactor = mockk(relaxed = true)
viewHolder = ExceptionsDeleteButtonViewHolder(view, interactor)
}
@Test
fun `calls onDeleteAll on click`() {
view.removeAllExceptions.performClick()
verify { interactor.onDeleteAll() }
}
}

View File

@ -1,56 +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.trackingprotectionexceptions.viewholders
import android.view.LayoutInflater
import android.view.View
import io.mockk.mockk
import io.mockk.verify
import kotlinx.android.synthetic.main.exception_item.view.*
import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
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.trackingprotectionexceptions.ExceptionsInteractor
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)
class ExceptionsListItemViewHolderTest {
private lateinit var view: View
private lateinit var interactor: ExceptionsInteractor
private lateinit var viewHolder: ExceptionsListItemViewHolder
@Before
fun setup() {
view = LayoutInflater.from(testContext)
.inflate(ExceptionsListItemViewHolder.LAYOUT_ID, null)
interactor = mockk(relaxed = true)
viewHolder = ExceptionsListItemViewHolder(view, interactor)
}
@Test
fun `bind url and icon`() {
val exception = object : TrackingProtectionException {
override val url = "https://example.com/icon.svg"
}
viewHolder.bind(exception)
assertEquals(exception.url, view.webAddressView.text)
}
@Test
fun `calls onDeleteOne on click`() {
val exception = object : TrackingProtectionException {
override val url = "https://example.com/icon.svg"
}
viewHolder.bind(exception)
view.delete_exception.performClick()
verify { interactor.onDeleteOne(exception) }
}
}