1
0
Fork 0

Add tests for login exceptions (#12681)

master
Tiger Oakes 2020-07-17 14:25:45 -07:00 committed by GitHub
parent aa31eb0fa5
commit 67fda80453
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 358 additions and 35 deletions

View File

@ -14,29 +14,22 @@ import org.mozilla.fenix.loginexceptions.viewholders.LoginExceptionsDeleteButton
import org.mozilla.fenix.loginexceptions.viewholders.LoginExceptionsHeaderViewHolder
import org.mozilla.fenix.loginexceptions.viewholders.LoginExceptionsListItemViewHolder
sealed class AdapterItem {
object DeleteButton : AdapterItem()
object Header : AdapterItem()
data class Item(val item: LoginException) : AdapterItem()
}
/**
* 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<AdapterItem, RecyclerView.ViewHolder>(DiffCallback) {
) : 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
)
val adapterItems: List<AdapterItem> = listOf(AdapterItem.Header) +
exceptions.map { AdapterItem.Item(it) } +
listOf(AdapterItem.DeleteButton)
submitList(adapterItems)
}
@ -70,9 +63,18 @@ class LoginExceptionsAdapter(
}
}
private object DiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
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) =
areContentsTheSame(oldItem, newItem)
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) =

View File

@ -9,9 +9,9 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
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.ExperimentalCoroutinesApi
@ -57,18 +57,15 @@ class LoginExceptionsFragment : Fragment() {
return view
}
private fun subscribeToLoginExceptions(): Observer<List<LoginException>> {
return Observer<List<LoginException>> { exceptions ->
exceptionsStore.dispatch(ExceptionsFragmentAction.Change(exceptions))
}.also { observer ->
requireComponents.core.loginExceptionStorage.getLoginExceptions().asLiveData()
.observe(viewLifecycleOwner, observer)
}
private fun subscribeToLoginExceptions() {
requireComponents.core.loginExceptionStorage.getLoginExceptions().asLiveData()
.observe(viewLifecycleOwner) { exceptions ->
exceptionsStore.dispatch(ExceptionsFragmentAction.Change(exceptions))
}
}
@ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
consumeFrom(exceptionsStore) {
exceptionsView.update(it)
}

View File

@ -10,7 +10,7 @@ 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.view.*
import kotlinx.android.synthetic.main.component_exceptions.*
import mozilla.components.feature.logins.exceptions.LoginException
import org.mozilla.fenix.R
@ -34,29 +34,29 @@ interface ExceptionsViewInteractor {
* View that contains and configures the Exceptions List
*/
class LoginExceptionsView(
override val containerView: ViewGroup,
container: ViewGroup,
val interactor: LoginExceptionsInteractor
) : LayoutContainer {
val view: FrameLayout = LayoutInflater.from(containerView.context)
.inflate(R.layout.component_exceptions, containerView, true)
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 {
view.exceptions_learn_more.isVisible = false
view.exceptions_empty_message.text =
view.context.getString(R.string.preferences_passwords_exceptions_description_empty)
view.exceptions_list.apply {
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) {
view.exceptions_empty_view.isVisible = state.items.isEmpty()
view.exceptions_list.isVisible = state.items.isNotEmpty()
exceptions_empty_view.isVisible = state.items.isEmpty()
exceptions_list.isVisible = state.items.isNotEmpty()
exceptionsAdapter.updateData(state.items)
}
}

View File

@ -12,12 +12,13 @@ import org.mozilla.fenix.R
class LoginExceptionsHeaderViewHolder(
view: View
) : RecyclerView.ViewHolder(view) {
companion object {
const val LAYOUT_ID = R.layout.exceptions_description
}
init {
view.exceptions_description.text =
view.context.getString(R.string.preferences_passwords_exceptions_description)
}
companion object {
const val LAYOUT_ID = R.layout.exceptions_description
}
}

View File

@ -0,0 +1,113 @@
/* 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

@ -0,0 +1,65 @@
/* 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.ViewGroup
import android.widget.FrameLayout
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import io.mockk.mockk
import kotlinx.android.synthetic.main.component_exceptions.*
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.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)
class LoginExceptionsViewTest {
private lateinit var parent: ViewGroup
private lateinit var interactor: LoginExceptionsInteractor
private lateinit var view: LoginExceptionsView
@Before
fun setup() {
parent = FrameLayout(testContext)
interactor = mockk()
view = LoginExceptionsView(parent, interactor)
}
@Test
fun `sets empty message text`() {
assertEquals(
"Logins and passwords that are not saved will be shown here.",
view.exceptions_empty_message.text
)
assertTrue(view.exceptions_list.adapter is LoginExceptionsAdapter)
assertTrue(view.exceptions_list.layoutManager is LinearLayoutManager)
}
@Test
fun `hide list when there are no items`() {
view.update(ExceptionsFragmentState(
items = emptyList()
))
assertTrue(view.exceptions_empty_view.isVisible)
assertFalse(view.exceptions_list.isVisible)
}
@Test
fun `shows list when there are items`() {
view.update(ExceptionsFragmentState(
items = listOf(mockk())
))
assertFalse(view.exceptions_empty_view.isVisible)
assertTrue(view.exceptions_list.isVisible)
}
}

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.loginexceptions.viewholders
import android.view.View
import com.google.android.material.button.MaterialButton
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 org.junit.Before
import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.loginexceptions.LoginExceptionsInteractor
class LoginExceptionsDeleteButtonViewHolderTest {
private lateinit var view: View
private lateinit var deleteButton: MaterialButton
private lateinit var interactor: LoginExceptionsInteractor
@Before
fun setup() {
deleteButton = mockk()
view = mockk {
every { findViewById<MaterialButton>(R.id.removeAllExceptions) } returns deleteButton
}
interactor = mockk()
}
@Test
fun `delete button calls interactor`() {
val slot = slot<View.OnClickListener>()
every { deleteButton.setOnClickListener(capture(slot)) } just Runs
LoginExceptionsDeleteButtonViewHolder(view, interactor)
every { interactor.onDeleteAll() } just Runs
slot.captured.onClick(mockk())
verify { interactor.onDeleteAll() }
}
}

View File

@ -0,0 +1,37 @@
/* 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.TextView
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.R
class LoginExceptionsHeaderViewHolderTest {
private lateinit var view: View
private lateinit var description: TextView
@Before
fun setup() {
description = mockk(relaxUnitFun = true)
view = mockk {
every { findViewById<TextView>(R.id.exceptions_description) } returns description
every {
context.getString(R.string.preferences_passwords_exceptions_description)
} returns "Logins and passwords will not be saved for these sites."
}
}
@Test
fun `sets description text`() {
LoginExceptionsHeaderViewHolder(view)
verify { description.text = "Logins and passwords will not be saved for these sites." }
}
}

View File

@ -0,0 +1,63 @@
/* 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) }
}
}