diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/IdlingResourceHelper.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/IdlingResourceHelper.kt new file mode 100644 index 000000000..860aeda76 --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/IdlingResourceHelper.kt @@ -0,0 +1,26 @@ +package org.mozilla.fenix.helpers + +import androidx.test.espresso.IdlingRegistry +import androidx.test.rule.ActivityTestRule +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.helpers.idlingresource.AddonsInstallingIdlingResource + +object IdlingResourceHelper { + + // Idling Resource to manage installing an addon + fun registerAddonInstallingIdlingResource(activityTestRule: ActivityTestRule) { + IdlingRegistry.getInstance().register( + AddonsInstallingIdlingResource( + activityTestRule.activity.supportFragmentManager + ) + ) + } + + fun unregisterAddonInstallingIdlingResource(activityTestRule: ActivityTestRule) { + IdlingRegistry.getInstance().unregister( + AddonsInstallingIdlingResource( + activityTestRule.activity.supportFragmentManager + ) + ) + } +} diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/idlingresource/AddonsInstallingIdlingResource.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/idlingresource/AddonsInstallingIdlingResource.kt new file mode 100644 index 000000000..a10fc21c8 --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/idlingresource/AddonsInstallingIdlingResource.kt @@ -0,0 +1,42 @@ +package org.mozilla.fenix.helpers.idlingresource + +import androidx.fragment.app.FragmentManager +import androidx.navigation.fragment.NavHostFragment +import androidx.test.espresso.IdlingResource +import mozilla.components.feature.addons.ui.AddonInstallationDialogFragment + +class AddonsInstallingIdlingResource( + val fragmentManager: FragmentManager +) : + IdlingResource { + private var resourceCallback: IdlingResource.ResourceCallback? = null + private var isAddonInstalled = false + + override fun getName(): String { + return this::javaClass.name + } + + override fun isIdleNow(): Boolean { + return isInstalledAddonDialogShown() + } + + override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) { + if (callback != null) + resourceCallback = callback + } + + private fun isInstalledAddonDialogShown(): Boolean { + val activityChildFragments = + (fragmentManager.fragments.first() as NavHostFragment) + .childFragmentManager.fragments + + for (childFragment in activityChildFragments.indices) { + if (activityChildFragments[childFragment] is AddonInstallationDialogFragment) { + resourceCallback?.onTransitionToIdle() + isAddonInstalled = true + return isAddonInstalled + } + } + return isAddonInstalled + } +} diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/idlingresource/AddonsLoadingIdlingResource.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/idlingresource/AddonsLoadingIdlingResource.kt new file mode 100644 index 000000000..56ff6cc06 --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/idlingresource/AddonsLoadingIdlingResource.kt @@ -0,0 +1,41 @@ +package org.mozilla.fenix.helpers.idlingresource + +import android.view.View +import android.view.View.VISIBLE +import androidx.fragment.app.FragmentManager +import androidx.test.espresso.IdlingResource +import org.mozilla.fenix.R +import org.mozilla.fenix.addons.AddonsManagementFragment + +class AddonsLoadingIdlingResource(val fragmentManager: FragmentManager) : IdlingResource { + private var resourceCallback: IdlingResource.ResourceCallback? = null + + override fun getName(): String { + return this::javaClass.name + } + + override fun isIdleNow(): Boolean { + val idle = addonsFinishedLoading() + if (idle) + resourceCallback?.onTransitionToIdle() + return idle + } + + override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) { + if (callback != null) + resourceCallback = callback + } + + private fun addonsFinishedLoading(): Boolean { + val progressbar = fragmentManager.findFragmentById(R.id.container)?.let { + val addonsManagementFragment = + it.childFragmentManager.fragments.first { it is AddonsManagementFragment } + addonsManagementFragment.view?.findViewById(R.id.add_ons_progress_bar) + } ?: return true + + if (progressbar.visibility == VISIBLE) + return false + + return true + } +} diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt new file mode 100644 index 000000000..7dd20c944 --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt @@ -0,0 +1,96 @@ +package org.mozilla.fenix.ui + +import org.mozilla.fenix.helpers.TestAssetHelper + +/* 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/. */ + +import okhttp3.mockwebserver.MockWebServer +import org.junit.Rule +import org.junit.Before +import org.junit.After +import org.junit.Test +import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.HomeActivityTestRule +import org.mozilla.fenix.ui.robots.homeScreen +import org.mozilla.fenix.ui.robots.navigationToolbar + +/** + * Tests for verifying the functionality of installing or removing addons + * + */ + +class SettingsAddonsTest { + /* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping. + + private lateinit var mockWebServer: MockWebServer + + @get:Rule + val activityTestRule = HomeActivityTestRule() + + @Before + fun setUp() { + mockWebServer = MockWebServer().apply { + setDispatcher(AndroidAssetDispatcher()) + start() + } + } + + @After + fun tearDown() { + mockWebServer.shutdown() + } + + // Walks through settings add-ons menu to ensure all items are present + @Test + fun settingsAddonsItemsTest() { + homeScreen { + }.openThreeDotMenu { + }.openSettings { + verifyAdvancedHeading() + verifyAddons() + }.openAddonsManagerMenu { + verifyAddonsItems() + } + } + + // Opens a webpage and installs an add-on from the three-dot menu + @Test + fun installAddonFromThreeDotMenu() { + val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + val addonName = "uBlock Origin" + + navigationToolbar { + }.openNewTabAndEnterToBrowser(defaultWebPage.url) { + }.openThreeDotMenu { + }.openAddonsManagerMenu { + clickInstallAddon(addonName) + verifyAddonPrompt(addonName) + cancelInstallAddon() + clickInstallAddon(addonName) + acceptInstallAddon() + verifyDownloadAddonPrompt(addonName, activityTestRule) + } + } + + // Opens the addons settings menu, installs an addon, then uninstalls + @Test + fun verifyAddonsCanBeUninstalled() { + val addonName = "uBlock Origin" + + homeScreen { + }.openThreeDotMenu { + }.openSettings { + verifyAdvancedHeading() + verifyAddons() + }.openAddonsManagerMenu { + clickInstallAddon(addonName) + acceptInstallAddon() + verifyDownloadAddonPrompt(addonName, activityTestRule) + }.openDetailedMenuForAddon(addonName) { + verifyCurrentAddonMenu() + }.removeAddon { + } + } +} diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt index 6d11be804..073148cde 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt @@ -19,6 +19,7 @@ import androidx.test.espresso.intent.matcher.IntentMatchers.toPackage import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.Visibility import androidx.test.espresso.matcher.ViewMatchers.hasDescendant +import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText @@ -74,7 +75,9 @@ class SettingsRobot { // ADVANCED SECTION fun verifyAdvancedHeading() = assertAdvancedHeading() - fun verifyAddons() = assertAddons() + fun verifyAddons() = assertAddonsButton() + + // DEVELOPER TOOLS SECTION fun verifyRemoteDebug() = assertRemoteDebug() fun verifyLeakCanaryButton() = assertLeakCanaryButton() @@ -211,6 +214,13 @@ class SettingsRobot { SettingsSubMenuDataCollectionRobot().interact() return SettingsSubMenuDataCollectionRobot.Transition() } + + fun openAddonsManagerMenu(interact: SettingsSubMenuAddonsManagerRobot.() -> Unit): SettingsSubMenuAddonsManagerRobot.Transition { + addonsManagerButton().click() + + SettingsSubMenuAddonsManagerRobot().interact() + return SettingsSubMenuAddonsManagerRobot.Transition() + } } companion object { @@ -349,15 +359,25 @@ private fun assertDeveloperToolsHeading() { // ADVANCED SECTION private fun assertAdvancedHeading() { - scrollToElementByText("Advanced") - onView(withText("Advanced")) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + onView(withId(R.id.recycler_view)).perform( + RecyclerViewActions.scrollTo( + hasDescendant(withText("Add-ons")) + ) + ) + + onView(withText("Add-ons")) + .check(matches(isCompletelyDisplayed())) } -private fun assertAddons() { - scrollToElementByText("Add-ons") - onView(withText("Add-ons")) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) +private fun assertAddonsButton() { + onView(withId(R.id.recycler_view)).perform( + RecyclerViewActions.scrollTo( + hasDescendant(withText("Add-ons")) + ) + ) + + addonsManagerButton() + .check(matches(isCompletelyDisplayed())) } private fun assertRemoteDebug() { @@ -414,5 +434,7 @@ fun isPackageInstalled(packageName: String): Boolean { } } +private fun addonsManagerButton() = onView(withText(R.string.preferences_addons)) + private fun goBackButton() = onView(CoreMatchers.allOf(ViewMatchers.withContentDescription("Navigate up"))) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerAddonDetailedMenuRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerAddonDetailedMenuRobot.kt new file mode 100644 index 000000000..93ca6f939 --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerAddonDetailedMenuRobot.kt @@ -0,0 +1,59 @@ +package org.mozilla.fenix.ui.robots + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withContentDescription +import androidx.test.espresso.matcher.ViewMatchers.withId +import org.hamcrest.CoreMatchers.allOf +import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.click + +/** + * Implementation of Robot Pattern for a single Addon inside of the Addons Management Settings. + */ + +class SettingsSubMenuAddonsManagerAddonDetailedMenuRobot { + + fun verifyCurrentAddonMenu() = assertAddonMenuItems() + + class Transition { + fun goBack(interact: SettingsSubMenuAddonsManagerRobot.() -> Unit): SettingsSubMenuAddonsManagerRobot.Transition { + fun goBackButton() = onView(allOf(withContentDescription("Navigate up"))) + goBackButton().click() + + SettingsSubMenuAddonsManagerRobot().interact() + return SettingsSubMenuAddonsManagerRobot.Transition() + } + + fun removeAddon(interact: SettingsSubMenuAddonsManagerRobot.() -> Unit): SettingsSubMenuAddonsManagerRobot.Transition { + removeAddonButton().click() + + SettingsSubMenuAddonsManagerRobot().interact() + return SettingsSubMenuAddonsManagerRobot.Transition() + } + } + + private fun assertAddonMenuItems() { + enableSwitchButton().check(matches(isCompletelyDisplayed())) + settingsButton().check(matches(isCompletelyDisplayed())) + detailsButton().check(matches(isCompletelyDisplayed())) + permissionsButton().check(matches(isCompletelyDisplayed())) + removeAddonButton().check(matches(isCompletelyDisplayed())) + } +} + +private fun enableSwitchButton() = + onView(withId(R.id.enable_switch)) + +private fun settingsButton() = + onView(withId(R.id.settings)) + +private fun detailsButton() = + onView(withId(R.id.details)) + +private fun permissionsButton() = + onView(withId(R.id.permissions)) + +private fun removeAddonButton() = + onView(withId(R.id.remove_add_on)) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerRobot.kt new file mode 100644 index 000000000..102367770 --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerRobot.kt @@ -0,0 +1,228 @@ +package org.mozilla.fenix.ui.robots + +import android.widget.RelativeLayout +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.hasDescendant +import androidx.test.espresso.matcher.ViewMatchers.hasSibling +import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom +import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA +import androidx.test.espresso.matcher.ViewMatchers.withContentDescription +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility +import androidx.test.espresso.matcher.ViewMatchers.Visibility +import androidx.test.espresso.matcher.ViewMatchers.withParent +import androidx.test.uiautomator.By +import androidx.test.uiautomator.Until +import org.hamcrest.CoreMatchers.allOf +import org.hamcrest.CoreMatchers.containsString +import org.hamcrest.CoreMatchers.instanceOf +import org.hamcrest.CoreMatchers.not +import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.HomeActivityTestRule +import org.mozilla.fenix.helpers.IdlingResourceHelper.registerAddonInstallingIdlingResource +import org.mozilla.fenix.helpers.IdlingResourceHelper.unregisterAddonInstallingIdlingResource +import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime +import org.mozilla.fenix.helpers.TestHelper +import org.mozilla.fenix.helpers.click +import org.mozilla.fenix.helpers.ext.waitNotNull + +/** + * Implementation of Robot Pattern for the Addons Management Settings. + */ + +class SettingsSubMenuAddonsManagerRobot { + fun verifyAddonPrompt(addonName: String) = assertAddonPrompt(addonName) + fun clickInstallAddon(addonName: String) = selectInstallAddon(addonName) + fun verifyDownloadAddonPrompt(addonName: String, activityTestRule: HomeActivityTestRule) = + assertDownloadingAddonPrompt(addonName, activityTestRule) + + fun cancelInstallAddon() = cancelInstall() + fun acceptInstallAddon() = allowInstall() + fun verifyAddonsItems() = assertAddonsItems() + fun verifyAddonCanBeInstalled(addonName: String) = assertAddonCanBeInstalled(addonName) + + class Transition { + fun goBack(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition { + fun goBackButton() = onView(allOf(withContentDescription("Navigate up"))) + goBackButton().click() + + HomeScreenRobot().interact() + return HomeScreenRobot.Transition() + } + + fun openDetailedMenuForAddon( + addonName: String, + interact: SettingsSubMenuAddonsManagerAddonDetailedMenuRobot.() -> Unit + ): SettingsSubMenuAddonsManagerAddonDetailedMenuRobot.Transition { + addonName.chars() + + onView( + allOf( + withId(R.id.add_on_item), + hasDescendant( + allOf( + withId(R.id.add_on_name), + withText(addonName) + ) + ) + ) + ).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + .perform(click()) + + SettingsSubMenuAddonsManagerAddonDetailedMenuRobot().interact() + return SettingsSubMenuAddonsManagerAddonDetailedMenuRobot.Transition() + } + } + + private fun installButtonForAddon(addonName: String) = + onView( + allOf( + withContentDescription(R.string.mozac_feature_addons_install_addon_content_description), + isDescendantOfA(withId(R.id.add_on_item)), + hasSibling(hasDescendant(withText(addonName))) + ) + ) + + private fun selectInstallAddon(addonName: String) { + mDevice.waitNotNull( + Until.findObject(By.textContains(addonName)), + waitingTime + ) + + installButtonForAddon(addonName) + .check(matches(isCompletelyDisplayed())) + .perform(click()) + } + + private fun assertAddonIsEnabled(addonName: String) { + installButtonForAddon(addonName) + .check(matches(not(isCompletelyDisplayed()))) + } + + private fun assertAddonPrompt(addonName: String) { + onView(allOf(withId(R.id.title), withText("Add $addonName?"))) + .check(matches(isCompletelyDisplayed())) + + onView( + allOf( + withId(R.id.permissions), + withText(containsString("It requires your permission to:")) + ) + ) + .check(matches(isCompletelyDisplayed())) + + onView(allOf(withId(R.id.allow_button), withText("Add"))) + .check(matches(isCompletelyDisplayed())) + + onView(allOf(withId(R.id.deny_button), withText("Cancel"))) + .check(matches(isCompletelyDisplayed())) + } + + private fun assertDownloadingAddonPrompt( + addonName: String, + activityTestRule: HomeActivityTestRule + ) { + registerAddonInstallingIdlingResource(activityTestRule) + + onView( + allOf( + withText("Okay, Got it"), + withParent(instanceOf(RelativeLayout::class.java)), + hasSibling(withText("$addonName has been added to Firefox Preview")), + hasSibling(withText("Open it in the menu")), + hasSibling(withText("Allow in private browsing")) + ) + ) + .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + .perform(click()) + + unregisterAddonInstallingIdlingResource(activityTestRule) + + TestHelper.scrollToElementByText(addonName) + + assertAddonIsInstalled(addonName) + } + + private fun assertAddonIsInstalled(addonName: String) { + onView( + allOf( + withId(R.id.add_button), + isDescendantOfA(withId(R.id.add_on_item)), + hasSibling(hasDescendant(withText(addonName))) + ) + ).check(matches(withEffectiveVisibility(Visibility.GONE))) + } + + private fun cancelInstall() { + onView(allOf(withId(R.id.deny_button), withText("Cancel"))) + .check(matches(isCompletelyDisplayed())) + .perform(click()) + } + + private fun allowInstall() { + onView(allOf(withId(R.id.allow_button), withText("Add"))) + .check(matches(isCompletelyDisplayed())) + .perform(click()) + } + + private fun assertAddonsItems() { + assertRecommendedTitleDisplayed() + assertAddons() + } + + private fun assertRecommendedTitleDisplayed() { + onView(allOf(withId(R.id.title), withText("Recommended"))) + .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + } + + private fun assertEnabledTitleDisplayed() { + onView(withText("Enabled")) + .check(matches(isCompletelyDisplayed())) + } + + private fun assertAddons() { + assertAddonUblock() + } + + private fun assertAddonUblock() { + onView( + allOf( + isAssignableFrom(RelativeLayout::class.java), + withId(R.id.add_on_item), + hasDescendant(allOf(withId(R.id.add_on_icon), isCompletelyDisplayed())), + hasDescendant( + allOf( + withId(R.id.details_container), + hasDescendant(withText("uBlock Origin")), + hasDescendant(withText("Finally, an efficient wide-spectrum content blocker. Easy on CPU and memory.")), + hasDescendant(withId(R.id.rating)), + hasDescendant(withId(R.id.users_count)) + ) + ), + hasDescendant(withId(R.id.add_button)) + ) + ).check(matches(isCompletelyDisplayed())) + } + + private fun assertAddonCanBeInstalled(addonName: String) { + device.waitNotNull(Until.findObject(By.text(addonName)), waitingTime) + + onView( + allOf( + withId(R.id.add_button), + hasSibling( + hasDescendant( + allOf( + withId(R.id.add_on_name), + withText(addonName) + ) + ) + ) + ) + ).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + } +} diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuMainRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuMainRobot.kt index 51665856b..fe335c79c 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuMainRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuMainRobot.kt @@ -347,6 +347,17 @@ class ThreeDotMenuMainRobot { ThreeDotMenuMainRobot().interact() return ThreeDotMenuMainRobot.Transition() } + + fun openAddonsManagerMenu(interact: SettingsSubMenuAddonsManagerRobot.() -> Unit): SettingsSubMenuAddonsManagerRobot.Transition { + clickAddonsManagerButton() + mDevice.waitNotNull( + Until.findObject(By.text("Recommended")), + waitingTime + ) + + SettingsSubMenuAddonsManagerRobot().interact() + return SettingsSubMenuAddonsManagerRobot.Transition() + } } } @@ -423,7 +434,9 @@ private fun addNewCollectionButton() = onView(allOf(withText("Add new collection private fun assertaddNewCollectionButton() = addNewCollectionButton() .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -private fun collectionNameTextField() = onView(allOf(withResourceName("name_collection_edittext"))) +private fun collectionNameTextField() = + onView(allOf(withResourceName("name_collection_edittext"))) + private fun assertCollectionNameTextField() = collectionNameTextField() .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) @@ -473,6 +486,7 @@ private fun assertReaderViewAppearanceButton(visible: Boolean) = readerViewAppea private fun addToFirefoxHomeButton() = onView(allOf(withText(R.string.browser_menu_add_to_top_sites))) + private fun assertAddToFirefoxHome() { onView(withId(R.id.mozac_browser_menu_recyclerView)) .perform( @@ -514,3 +528,10 @@ private fun assertOpenInAppButton() { ) ).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) } + +private fun addonsManagerButton() = onView(withText("Add-ons Manager")) + +private fun clickAddonsManagerButton() { + onView(withText("Add-ons")).click() + addonsManagerButton().click() +}