1
0
Fork 0

Add tests for addon details

master
Tiger Oakes 2020-06-15 16:36:46 -07:00 committed by Emily Kager
parent 48f234fdac
commit f6efb386d6
5 changed files with 257 additions and 104 deletions

View File

@ -7,13 +7,10 @@ package org.mozilla.fenix.addons
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.View
import androidx.core.text.HtmlCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.navArgs
import kotlinx.android.synthetic.main.fragment_add_on_details.view.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
@ -21,108 +18,35 @@ import kotlinx.coroutines.withContext
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.ui.showInformationDialog
import mozilla.components.feature.addons.ui.translatedName
import mozilla.components.feature.addons.ui.translatedDescription
import mozilla.components.feature.addons.update.DefaultAddonUpdater.UpdateAttemptStorage
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.showToolbar
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Locale
/**
* A fragment to show the details of an add-on.
*/
class AddonDetailsFragment : Fragment(R.layout.fragment_add_on_details) {
private val updateAttemptStorage: UpdateAttemptStorage by lazy {
UpdateAttemptStorage(requireContext())
}
class AddonDetailsFragment : Fragment(R.layout.fragment_add_on_details), AddonDetailsInteractor {
private val updateAttemptStorage by lazy { UpdateAttemptStorage(requireContext()) }
private val args by navArgs<AddonDetailsFragmentArgs>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
bind(args.addon, view)
showToolbar(title = args.addon.translatedName)
AddonDetailsView(view, interactor = this).bind(args.addon)
}
private fun bind(addon: Addon, view: View) {
val title = addon.translatedName
showToolbar(title)
bindDetails(addon, view)
bindAuthors(addon, view)
bindVersion(addon, view)
bindLastUpdated(addon, view)
bindWebsite(addon, view)
bindRating(addon, view)
override fun openWebsite(addonSiteUrl: Uri) {
startActivity(Intent(Intent.ACTION_VIEW, addonSiteUrl))
}
private fun bindRating(addon: Addon, view: View) {
addon.rating?.let {
val ratingView = view.rating_view
val userCountView = view.users_count
val ratingContentDescription =
getString(R.string.mozac_feature_addons_rating_content_description)
ratingView.contentDescription = String.format(ratingContentDescription, it.average)
ratingView.rating = it.average
userCountView.text = getFormattedAmount(it.reviews)
}
}
private fun bindWebsite(addon: Addon, view: View) {
view.home_page_label.setOnClickListener {
val intent =
Intent(Intent.ACTION_VIEW).setData(Uri.parse(addon.siteUrl))
startActivity(intent)
}
}
private fun bindLastUpdated(addon: Addon, view: View) {
view.last_updated_text.text = formatDate(addon.updatedAt)
}
private fun bindVersion(addon: Addon, view: View) {
view.version_text.text =
addon.installedState?.version?.ifEmpty { addon.version } ?: addon.version
if (addon.isInstalled()) {
view.version_text.setOnLongClickListener {
showUpdaterDialog(addon)
true
override fun showUpdaterDialog(addon: Addon) {
viewLifecycleOwner.lifecycleScope.launch(Main) {
val updateAttempt = withContext(IO) {
updateAttemptStorage.findUpdateAttemptBy(addon.id)
}
updateAttempt?.showInformationDialog(requireContext())
}
}
private fun showUpdaterDialog(addon: Addon) {
viewLifecycleOwner.lifecycleScope.launch(IO) {
val updateAttempt = updateAttemptStorage.findUpdateAttemptBy(addon.id)
updateAttempt?.let {
withContext(Main) {
it.showInformationDialog(requireContext())
}
}
}
}
private fun bindAuthors(addon: Addon, view: View) {
view.author_text.text = addon.authors.joinToString { author ->
author.name
}.trim()
}
private fun bindDetails(addon: Addon, view: View) {
val detailsView = view.details
val detailsText = addon.translatedDescription
val parsedText = detailsText.replace("\n", "<br/>")
val text = HtmlCompat.fromHtml(parsedText, HtmlCompat.FROM_HTML_MODE_COMPACT)
detailsView.text = text
detailsView.movementMethod = LinkMovementMethod.getInstance()
}
private fun formatDate(text: String): String {
val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault())
return DateFormat.getDateInstance().format(formatter.parse(text)!!)
}
}

View File

@ -0,0 +1,112 @@
/* 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.addons
import android.net.Uri
import android.text.method.LinkMovementMethod
import android.view.View
import androidx.core.net.toUri
import androidx.core.text.HtmlCompat
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.fragment_add_on_details.*
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.ui.translatedDescription
import org.mozilla.fenix.R
import java.text.DateFormat
import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.Locale
interface AddonDetailsInteractor {
/**
* Open the given addon siteUrl in the browser.
*/
fun openWebsite(addonSiteUrl: Uri)
/**
* Display the updater dialog.
*/
fun showUpdaterDialog(addon: Addon)
}
/**
* Shows the details of an add-on.
*/
class AddonDetailsView(
override val containerView: View,
private val interactor: AddonDetailsInteractor
) : LayoutContainer {
private val dateParser = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault())
private val dateFormatter = DateFormat.getDateInstance()
private val numberFormatter = NumberFormat.getNumberInstance(Locale.getDefault())
fun bind(addon: Addon) {
bindDetails(addon)
bindAuthors(addon)
bindVersion(addon)
bindLastUpdated(addon)
bindWebsite(addon)
bindRating(addon)
}
private fun bindRating(addon: Addon) {
addon.rating?.let { rating ->
val resources = containerView.resources
val ratingContentDescription =
resources.getString(R.string.mozac_feature_addons_rating_content_description)
rating_view.contentDescription = String.format(ratingContentDescription, rating.average)
rating_view.rating = rating.average
users_count.text = numberFormatter.format(rating.reviews)
}
}
private fun bindWebsite(addon: Addon) {
home_page_label.setOnClickListener {
interactor.openWebsite(addon.siteUrl.toUri())
}
}
private fun bindLastUpdated(addon: Addon) {
last_updated_text.text = formatDate(addon.updatedAt)
}
private fun bindVersion(addon: Addon) {
var version = addon.installedState?.version
if (version.isNullOrEmpty()) {
version = addon.version
}
version_text.text = version
if (addon.isInstalled()) {
version_text.setOnLongClickListener {
interactor.showUpdaterDialog(addon)
true
}
} else {
version_text.setOnLongClickListener(null)
}
}
private fun bindAuthors(addon: Addon) {
author_text.text = addon.authors.joinToString { author -> author.name }.trim()
}
private fun bindDetails(addon: Addon) {
val detailsText = addon.translatedDescription
val parsedText = detailsText.replace("\n", "<br/>")
val text = HtmlCompat.fromHtml(parsedText, HtmlCompat.FROM_HTML_MODE_COMPACT)
details.text = text
details.movementMethod = LinkMovementMethod.getInstance()
}
private fun formatDate(text: String): String {
return dateFormatter.format(dateParser.parse(text)!!)
}
}

View File

@ -13,7 +13,7 @@ import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.Navigation
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.fragment_add_ons_management.*
import kotlinx.android.synthetic.main.fragment_add_ons_management.view.*
@ -23,10 +23,10 @@ import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.AddonManagerException
import mozilla.components.feature.addons.ui.AddonInstallationDialogFragment
import mozilla.components.feature.addons.ui.AddonsManagerAdapter
import mozilla.components.feature.addons.ui.AddonsManagerAdapterDelegate
import mozilla.components.feature.addons.ui.PermissionsDialogFragment
import mozilla.components.feature.addons.ui.AddonInstallationDialogFragment
import mozilla.components.feature.addons.ui.translatedName
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
@ -140,7 +140,7 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management),
AddonsManagementFragmentDirections.actionAddonsManagementFragmentToInstalledAddonDetails(
addon
)
Navigation.findNavController(requireView()).navigate(directions)
findNavController().navigate(directions)
}
private fun showDetailsFragment(addon: Addon) {
@ -148,7 +148,7 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management),
AddonsManagementFragmentDirections.actionAddonsManagementFragmentToAddonDetailsFragment(
addon
)
Navigation.findNavController(requireView()).navigate(directions)
findNavController().navigate(directions)
}
private fun showNotYetSupportedAddonFragment(unsupportedAddons: ArrayList<Addon>) {
@ -156,7 +156,7 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management),
AddonsManagementFragmentDirections.actionAddonsManagementFragmentToNotYetSupportedAddonFragment(
unsupportedAddons.toTypedArray()
)
Navigation.findNavController(requireView()).navigate(directions)
findNavController().navigate(directions)
}
private fun findPreviousDialogFragment(): PermissionsDialogFragment? {

View File

@ -7,17 +7,6 @@ package org.mozilla.fenix.addons
import android.view.View
import androidx.fragment.app.Fragment
import org.mozilla.fenix.components.FenixSnackbar
import java.text.NumberFormat
import java.util.Locale
/**
* Get the formatted number amount for the current default locale.
*
* @param amount The number of addons to be formatted for the current default locale..
*/
internal fun getFormattedAmount(amount: Int): String {
return NumberFormat.getNumberInstance(Locale.getDefault()).format(amount)
}
/**
* Shows the Fenix Snackbar in the given view along with the provided text.

View File

@ -0,0 +1,128 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.addons
import android.net.Uri
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
import android.view.View
import io.mockk.mockk
import io.mockk.verify
import kotlinx.android.synthetic.main.fragment_add_on_details.view.*
import mozilla.components.feature.addons.Addon
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
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
@RunWith(FenixRobolectricTestRunner::class)
class AddonDetailsViewTest {
private lateinit var view: View
private lateinit var interactor: AddonDetailsInteractor
private lateinit var detailsView: AddonDetailsView
private val baseAddon = Addon(
id = "",
translatableDescription = mapOf(
Addon.DEFAULT_LOCALE to "Some blank addon\nwith a blank line"
),
updatedAt = "2020-11-23T08:00:00Z"
)
@Before
fun setup() {
view = LayoutInflater.from(testContext).inflate(R.layout.fragment_add_on_details, null)
interactor = mockk(relaxed = true)
detailsView = AddonDetailsView(view, interactor)
}
@Test
fun `bind addons rating`() {
detailsView.bind(baseAddon.copy(
rating = null
))
assertEquals(0f, view.rating_view.rating)
detailsView.bind(baseAddon.copy(
rating = Addon.Rating(
average = 4.3f,
reviews = 100
)
))
assertEquals("4.30/5", view.rating_view.contentDescription)
assertEquals(4.5f, view.rating_view.rating)
assertEquals("100", view.users_count.text)
}
@Test
fun `bind addons website`() {
detailsView.bind(baseAddon.copy(
siteUrl = "https://mozilla.org"
))
view.home_page_label.performClick()
verify { interactor.openWebsite(Uri.parse("https://mozilla.org")) }
}
@Test
fun `bind addons last updated`() {
detailsView.bind(baseAddon)
assertEquals("Nov 23, 2020", view.last_updated_text.text)
}
@Test
fun `bind addons version`() {
detailsView.bind(baseAddon.copy(
version = "1.0.0",
installedState = null
))
assertEquals("1.0.0", view.version_text.text)
view.version_text.performLongClick()
verify(exactly = 0) { interactor.showUpdaterDialog(any()) }
detailsView.bind(baseAddon.copy(
version = "1.0.0",
installedState = Addon.InstalledState(
id = "",
version = "2.0.0",
optionsPageUrl = null
)
))
assertEquals("2.0.0", view.version_text.text)
view.version_text.performLongClick()
verify { interactor.showUpdaterDialog(any()) }
}
@Test
fun `bind addons authors`() {
val baseAuthor = Addon.Author("", "", "", "")
detailsView.bind(baseAddon.copy(
authors = listOf(
baseAuthor.copy(name = " Sarah Jane"),
baseAuthor.copy(name = "John Smith ")
)
))
assertEquals("Sarah Jane, John Smith", view.author_text.text)
}
@Test
fun `bind addons details`() {
detailsView.bind(baseAddon)
assertEquals(
"Some blank addon\nwith a blank line",
view.details.text.toString()
)
assertTrue(view.details.movementMethod is LinkMovementMethod)
}
}