From f6efb386d699d6a5559f681992c43305fbc8e7ad Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Mon, 15 Jun 2020 16:36:46 -0700 Subject: [PATCH] Add tests for addon details --- .../fenix/addons/AddonDetailsFragment.kt | 100 ++------------ .../mozilla/fenix/addons/AddonDetailsView.kt | 112 +++++++++++++++ .../fenix/addons/AddonsManagementFragment.kt | 10 +- .../org/mozilla/fenix/addons/Extensions.kt | 11 -- .../fenix/addons/AddonDetailsViewTest.kt | 128 ++++++++++++++++++ 5 files changed, 257 insertions(+), 104 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/addons/AddonDetailsView.kt create mode 100644 app/src/test/java/org/mozilla/fenix/addons/AddonDetailsViewTest.kt diff --git a/app/src/main/java/org/mozilla/fenix/addons/AddonDetailsFragment.kt b/app/src/main/java/org/mozilla/fenix/addons/AddonDetailsFragment.kt index b20491bfa..6548e311c 100644 --- a/app/src/main/java/org/mozilla/fenix/addons/AddonDetailsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/addons/AddonDetailsFragment.kt @@ -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() 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", "
") - 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)!!) - } } diff --git a/app/src/main/java/org/mozilla/fenix/addons/AddonDetailsView.kt b/app/src/main/java/org/mozilla/fenix/addons/AddonDetailsView.kt new file mode 100644 index 000000000..372e3e8b5 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/addons/AddonDetailsView.kt @@ -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", "
") + 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)!!) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt b/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt index b19afb3bf..9afcb871f 100644 --- a/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt @@ -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) { @@ -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? { diff --git a/app/src/main/java/org/mozilla/fenix/addons/Extensions.kt b/app/src/main/java/org/mozilla/fenix/addons/Extensions.kt index 4e0199933..6c8a991f0 100644 --- a/app/src/main/java/org/mozilla/fenix/addons/Extensions.kt +++ b/app/src/main/java/org/mozilla/fenix/addons/Extensions.kt @@ -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. diff --git a/app/src/test/java/org/mozilla/fenix/addons/AddonDetailsViewTest.kt b/app/src/test/java/org/mozilla/fenix/addons/AddonDetailsViewTest.kt new file mode 100644 index 000000000..288c5d2f5 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/addons/AddonDetailsViewTest.kt @@ -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) + } +}