Merge remote-tracking branch 'origin/fenix/117.0' into iceraven

pull/700/head
akliuxingyuan 9 months ago
commit 8c13773efd

@ -1 +1 @@
Subproject commit 10bdc0f0a180ea2e892068ebd25e2e9183eb1c95
Subproject commit c8b0ad246e4eeaf545298fbc8d0370edb0f3ee9c

@ -43,7 +43,7 @@ juno-onboarding:
type: boolean
description: "if true, juno onboarding is shown to the user."
messaging:
description: "Configuration for the messaging system.\n\nIn practice this is a set of growable lookup tables for the\nmessage controller to piece together.\n"
description: "The in-app messaging system.\n"
hasExposure: true
exposureDescription: ""
variables:
@ -52,7 +52,7 @@ messaging:
description: A growable map of action URLs.
message-under-experiment:
type: string
description: Id or prefix of the message under experiment.
description: "Deprecated in favor of `MessageData#experiment`. This will be removed in future releases."
messages:
type: json
description: A growable collection of messages
@ -128,6 +128,17 @@ pre-permission-notification-prompt:
enabled:
type: boolean
description: "if true, the pre-permission notification prompt is shown to the user."
print:
description: A feature for printing from the share or browser menu.
hasExposure: true
exposureDescription: ""
variables:
browser-print-enabled:
type: boolean
description: "If true, a print button from the browser menu is available."
share-print-enabled:
type: boolean
description: "If true, a print button from the share menu is available."
re-engagement-notification:
description: A feature that shows the re-engagement notification if the user is inactive.
hasExposure: true
@ -139,25 +150,31 @@ re-engagement-notification:
type:
type: int
description: The type of re-engagement notification that is shown to the inactive user.
search-term-groups:
description: A feature allowing the grouping of URLs around the search term that it came from.
search-extra-params:
description: A feature that provides additional args for search.
hasExposure: true
exposureDescription: ""
variables:
channel-id:
type: json
description: The channel Id param name with arg.
enabled:
type: boolean
description: "If true, the feature shows up on the homescreen and on the new tab screen."
search_extra_params:
description: A feature that provides a search engine name and a channel ID.
description: "If true, the feature is active."
feature-enabler:
type: json
description: "The feature enabler param name with arg, NOTE this map could be empty."
search-engine:
type: string
description: The search engine name.
search-term-groups:
description: A feature allowing the grouping of URLs around the search term that it came from.
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "If true, the feature is active."
search_name_channel_id:
type: json
description: The search engine name and the channel ID.
description: "If true, the feature shows up on the homescreen and on the new tab screen."
shopping-experience:
description: A feature that shows product review quality information.
hasExposure: true

@ -1,5 +1,4 @@
import org.apache.tools.ant.util.StringUtils
import org.mozilla.fenix.gradle.tasks.ApkSizeTask
plugins {
id "com.jetbrains.python.envs" version "$python_envs_plugin"
@ -632,8 +631,8 @@ dependencies {
implementation FenixDependencies.androidx_paging
implementation ComponentsDependencies.androidx_preferences
implementation ComponentsDependencies.androidx_fragment
implementation FenixDependencies.androidx_navigation_fragment
implementation FenixDependencies.androidx_navigation_ui
implementation ComponentsDependencies.androidx_navigation_fragment
implementation ComponentsDependencies.androidx_navigation_ui
implementation ComponentsDependencies.androidx_recyclerview
implementation FenixDependencies.androidx_lifecycle_common
implementation ComponentsDependencies.androidx_lifecycle_livedata

@ -563,6 +563,95 @@ events:
metadata:
tags:
- Sharing
print_failure:
type: event
description: |
A user tapped the print in the menu or share sheet but an error occurred
and the process failed.
extra_keys:
source:
type: string
description: |
A string that indicates the type of document of pdf, non-pdf or unknown.
The default is unknown.
reason:
type: string
description: |
An error occurred while setting up for printing.
Default option is unknown, other options are no_settings_service, no_settings,
no_canonical_context, no_activity_context_delegate, no_activity_context,
no_print_delegate, and io_error.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1837517
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1837517#c3
data_sensitivity:
- technical
notification_emails:
- android-probes@mozilla.com
expires: 122
print_completed:
type: event
description: |
Printing from the share sheet or menu successfully completed.
extra_keys:
source:
type: string
description: |
A string that indicates the type of document of pdf, non-pdf or unknown.
The default is unknown.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1837517
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1837517#c3
data_sensitivity:
- technical
notification_emails:
- android-probes@mozilla.com
expires: 122
print_tapped:
type: event
description: |
A user tapped the print option in the menu or share sheet.
extra_keys:
source:
type: string
description: |
A string that indicates the type of document of pdf, non-pdf or unknown.
The default is unknown.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1837517
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1837517#c5
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 122
share_menu_action:
type: event
description: |
A share menu item was tapped.
The name of the item that the user tapped is stored in extras with the
key `item`.
extra_keys:
item:
description: |
A string containing the name of the item the user tapped. These items
currently include: print
type: string
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1837517
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1837517#c5
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 122
metadata:
tags:
- Sharing
share_to_app:
type: event
description: |
@ -2475,6 +2564,25 @@ metrics:
metadata:
tags:
- Experiments
search_page_load_time:
type: timing_distribution
time_unit: millisecond
lifetime: application
description: The time that it takes to load the Search content.
send_in_pings:
- metrics
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1842604
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/2889
data_sensitivity:
- technical
notification_emails:
- android-probes@mozilla.com
expires: 128
metadata:
tags:
- Experiments
customize_home:
most_visited_sites:
@ -3986,6 +4094,40 @@ settings:
tags:
- Sync
- Settings
passwords:
type: event
description: |
User has tapped Passwords in settings menu.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1841152
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/2802
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
- cgordon@mozilla.com
expires: never
metadata:
tags:
- Settings
autofill:
type: event
description: |
User has tapped Autofill in settings menu.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1841152
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/2802
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
- cgordon@mozilla.com
expires: never
metadata:
tags:
- Settings
history:
opened:
type: event
@ -5957,6 +6099,91 @@ media_state:
- Media
logins:
autofill_prompt_shown:
type: event
description: |
Password autofill prompt was shown.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1841152
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/2802
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
- cgordon@mozilla.com
expires: never
metadata:
tags:
- Logins
autofill_prompt_dismissed:
type: event
description: |
Password autofill prompt was dismissed.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1841152
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/2802
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
- cgordon@mozilla.com
expires: never
metadata:
tags:
- Logins
autofilled:
type: event
description: |
Password autofill performed.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1841152
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/2802
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
- cgordon@mozilla.com
expires: never
metadata:
tags:
- Logins
management_add_tapped:
type: event
description: |
User has tapped the add button through password management in settings menu.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1841152
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/2802
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
- cgordon@mozilla.com
expires: never
metadata:
tags:
- Logins
management_logins_tapped:
type: event
description: |
User has tapped on a saved login through password management in settings menu.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1841152
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/2802
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
- cgordon@mozilla.com
expires: never
metadata:
tags:
- Logins
open_logins:
type: event
description: |
@ -6684,6 +6911,20 @@ top_sites:
tags:
- Shortcuts
app_menu:
customize_homepage:
type: event
description: |
User has tapped on Customize Homepage in the app menu.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1841156
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/2726
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
- cgordon@mozilla.com
expires: never
sign_into_sync:
type: counter
description: |
@ -8942,6 +9183,25 @@ home_screen:
- cgordon@mozilla.com
expires: never
homepage:
private_mode_icon_tapped:
type: event
description: |
A user tapped the private browsing icon on the homepage.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1841153
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/2725
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
- cgordon@mozilla.com
expires: never
metadata:
tags:
- Tabs
start_on_home:
enter_home_screen:
type: event
@ -9974,3 +10234,38 @@ pull_to_refresh_in_browser:
notification_emails:
- android-probes@mozilla.com
expires: 126
app_icon:
new_private_tab_tapped:
type: event
description: |
A user tapped to open new private tab in Firefox Android app shortcuts menu.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1841153
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/2725
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
- cgordon@mozilla.com
expires: never
metadata:
tags:
- Tabs
sync:
failed:
type: event
description: |
User sign-in/sync has failed.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1841156
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/2726
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
- cgordon@mozilla.com
expires: never

@ -344,15 +344,35 @@ features:
value:
enabled: true
search_extra_params:
description: A feature that provides a search engine name and a channel ID.
print:
description: A feature for printing from the share or browser menu.
variables:
share-print-enabled:
description: If true, a print button from the share menu is available.
type: Boolean
default: true
browser-print-enabled:
description: If true, a print button from the browser menu is available.
type: Boolean
default: true
search-extra-params:
description: A feature that provides additional args for search.
variables:
enabled:
description: If true, the feature is active.
type: Boolean
default: false
search_name_channel_id:
description: The search engine name and the channel ID.
search-engine:
description: The search engine name.
type: String
default: ""
feature-enabler:
description: The feature enabler param name with arg, NOTE this map could be empty.
type: Map<String, String>
default: {}
channel-id:
description: The channel Id param name with arg.
type: Map<String, String>
default: {}
types:

@ -82,11 +82,6 @@ interface FeatureSettingsHelper {
*/
var tabsTrayRewriteEnabled: Boolean
/**
* Enable or disable the Unified search feature.
*/
var isUnifiedSearchEnabled: Boolean
fun applyFlagUpdates()
fun resetAllFeatureFlags()

@ -31,14 +31,12 @@ class FeatureSettingsHelperDelegate() : FeatureSettingsHelper {
isRecentlyVisitedFeatureEnabled = settings.historyMetadataUIFeature,
isPWAsPromptEnabled = !settings.userKnowsAboutPwas,
isTCPCFREnabled = settings.shouldShowTotalCookieProtectionCFR,
isUnifiedSearchEnabled = false,
isWallpaperOnboardingEnabled = settings.showWallpaperOnboarding,
isDeleteSitePermissionsEnabled = settings.deleteSitePermissions,
isCookieBannerReductionDialogEnabled = !settings.userOptOutOfReEngageCookieBannerDialog,
isOpenInAppBannerEnabled = settings.shouldShowOpenInAppBanner,
etpPolicy = getETPPolicy(settings),
tabsTrayRewriteEnabled = settings.enableTabsTrayToCompose,
newSearchSettingsEnabled = false,
)
/**
@ -57,7 +55,6 @@ class FeatureSettingsHelperDelegate() : FeatureSettingsHelper {
}
}
override var isUnifiedSearchEnabled: Boolean by updatedFeatureFlags::isUnifiedSearchEnabled
override var isPocketEnabled: Boolean by updatedFeatureFlags::isPocketEnabled
override var isJumpBackInCFREnabled: Boolean by updatedFeatureFlags::isJumpBackInCFREnabled
override var isWallpaperOnboardingEnabled: Boolean by updatedFeatureFlags::isWallpaperOnboardingEnabled
@ -89,13 +86,11 @@ class FeatureSettingsHelperDelegate() : FeatureSettingsHelper {
settings.historyMetadataUIFeature = featureFlags.isRecentlyVisitedFeatureEnabled
settings.userKnowsAboutPwas = !featureFlags.isPWAsPromptEnabled
settings.shouldShowTotalCookieProtectionCFR = featureFlags.isTCPCFREnabled
settings.showUnifiedSearchFeature = featureFlags.isUnifiedSearchEnabled
settings.showWallpaperOnboarding = featureFlags.isWallpaperOnboardingEnabled
settings.deleteSitePermissions = featureFlags.isDeleteSitePermissionsEnabled
settings.userOptOutOfReEngageCookieBannerDialog = !featureFlags.isCookieBannerReductionDialogEnabled
settings.shouldShowOpenInAppBanner = featureFlags.isOpenInAppBannerEnabled
settings.enableTabsTrayToCompose = featureFlags.tabsTrayRewriteEnabled
settings.enableUnifiedSearchSettingsUI = featureFlags.newSearchSettingsEnabled
setETPPolicy(featureFlags.etpPolicy)
}
}
@ -109,14 +104,12 @@ private data class FeatureFlags(
var isRecentlyVisitedFeatureEnabled: Boolean,
var isPWAsPromptEnabled: Boolean,
var isTCPCFREnabled: Boolean,
var isUnifiedSearchEnabled: Boolean,
var isWallpaperOnboardingEnabled: Boolean,
var isDeleteSitePermissionsEnabled: Boolean,
var isCookieBannerReductionDialogEnabled: Boolean,
var isOpenInAppBannerEnabled: Boolean,
var etpPolicy: ETPPolicy,
var tabsTrayRewriteEnabled: Boolean,
var newSearchSettingsEnabled: Boolean,
)
internal fun getETPPolicy(settings: Settings): ETPPolicy {

@ -12,13 +12,14 @@ import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.espresso.intent.rule.IntentsTestRule
import androidx.test.rule.ActivityTestRule
import androidx.test.uiautomator.UiSelector
import org.junit.rules.TestRule
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.helpers.FeatureSettingsHelper.Companion.settings
import org.mozilla.fenix.helpers.TestHelper.appContext
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.onboarding.FenixOnboarding
typealias HomeActivityComposeTestRule = AndroidComposeTestRule<HomeActivityTestRule, HomeActivity>
typealias HomeActivityComposeTestRule = AndroidComposeTestRule<out TestRule, HomeActivity>
/**
* A [org.junit.Rule] to handle shared test set up for tests on [HomeActivity].
@ -55,7 +56,6 @@ class HomeActivityTestRule(
isOpenInAppBannerEnabled: Boolean = settings.shouldShowOpenInAppBanner,
etpPolicy: ETPPolicy = getETPPolicy(settings),
tabsTrayRewriteEnabled: Boolean = false,
isUnifiedSearchEnabled: Boolean = false,
) : this(initialTouchMode, launchActivity, skipOnboarding) {
this.isHomeOnboardingDialogEnabled = isHomeOnboardingDialogEnabled
this.isPocketEnabled = isPocketEnabled
@ -70,7 +70,6 @@ class HomeActivityTestRule(
this.isOpenInAppBannerEnabled = isOpenInAppBannerEnabled
this.etpPolicy = etpPolicy
this.tabsTrayRewriteEnabled = tabsTrayRewriteEnabled
this.isUnifiedSearchEnabled = isUnifiedSearchEnabled
}
/**
@ -126,7 +125,6 @@ class HomeActivityTestRule(
isWallpaperOnboardingEnabled = false,
isCookieBannerReductionDialogEnabled = false,
isOpenInAppBannerEnabled = false,
isUnifiedSearchEnabled = false,
)
}
}
@ -160,7 +158,6 @@ class HomeActivityIntentTestRule internal constructor(
isRecentlyVisitedFeatureEnabled: Boolean = settings.historyMetadataUIFeature,
isPWAsPromptEnabled: Boolean = !settings.userKnowsAboutPwas,
isTCPCFREnabled: Boolean = settings.shouldShowTotalCookieProtectionCFR,
isUnifiedSearchEnabled: Boolean = false,
isWallpaperOnboardingEnabled: Boolean = settings.showWallpaperOnboarding,
isDeleteSitePermissionsEnabled: Boolean = settings.deleteSitePermissions,
isCookieBannerReductionDialogEnabled: Boolean = !settings.userOptOutOfReEngageCookieBannerDialog,
@ -175,7 +172,6 @@ class HomeActivityIntentTestRule internal constructor(
this.isRecentlyVisitedFeatureEnabled = isRecentlyVisitedFeatureEnabled
this.isPWAsPromptEnabled = isPWAsPromptEnabled
this.isTCPCFREnabled = isTCPCFREnabled
this.isUnifiedSearchEnabled = isUnifiedSearchEnabled
this.isWallpaperOnboardingEnabled = isWallpaperOnboardingEnabled
this.isDeleteSitePermissionsEnabled = isDeleteSitePermissionsEnabled
this.isCookieBannerReductionDialogEnabled = isCookieBannerReductionDialogEnabled
@ -263,7 +259,6 @@ class HomeActivityIntentTestRule internal constructor(
launchActivity: Boolean = true,
skipOnboarding: Boolean = false,
tabsTrayRewriteEnabled: Boolean = false,
isUnifiedSearchEnabled: Boolean = false,
) = HomeActivityIntentTestRule(
initialTouchMode = initialTouchMode,
launchActivity = launchActivity,
@ -272,7 +267,6 @@ class HomeActivityIntentTestRule internal constructor(
isJumpBackInCFREnabled = false,
isPWAsPromptEnabled = false,
isTCPCFREnabled = false,
isUnifiedSearchEnabled = isUnifiedSearchEnabled,
isWallpaperOnboardingEnabled = false,
isCookieBannerReductionDialogEnabled = false,
isOpenInAppBannerEnabled = false,

@ -0,0 +1,120 @@
/* 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.helpers
import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.runBlocking
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.icons.IconRequest
import mozilla.components.browser.icons.generator.DefaultIconGenerator
import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
import mozilla.components.browser.storage.sync.PlacesHistoryStorage
import mozilla.components.concept.storage.PageVisit
import mozilla.components.concept.storage.VisitType
import mozilla.components.feature.search.ext.createSearchEngine
import okhttp3.mockwebserver.MockWebServer
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.TestHelper.appContext
import org.mozilla.fenix.search.SearchEngineSource.None.searchEngine
object MockBrowserDataHelper {
val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
/**
* Adds a new bookmark item, visible in the Bookmarks folder.
*
* @param url The URL of the bookmark item to add. URLs should use the "https://example.com" format.
* @param title The title of the bookmark item to add.
* @param position Example for the position param: 1u, 2u, etc.
*/
fun createBookmarkItem(url: String, title: String, position: UInt?) {
runBlocking {
PlacesBookmarksStorage(context)
.addItem(
BookmarkRoot.Mobile.id,
url,
title,
position,
)
}
}
/**
* Adds a new history item, visible in the History folder.
*
* @param url The URL of the history item to add. URLs should use the "https://example.com" format.
*/
fun createHistoryItem(url: String) {
runBlocking {
PlacesHistoryStorage(appContext)
.recordVisit(
url,
PageVisit(VisitType.LINK),
)
}
}
/**
* Creates a new tab with a webpage, also visible in the History folder.
*
* URLs should use the "https://example.com" format.
*/
fun createTabItem(url: String) {
runBlocking {
appContext.components.useCases.tabsUseCases.addTab(url)
}
}
/**
* Triggers a search for the provided search term in a new tab.
*
*/
fun createSearchHistory(searchTerm: String) {
appContext.components.useCases.searchUseCases.newTabSearch.invoke(searchTerm)
}
/**
* Creates a new custom search engine object.
*
* @param mockWebServer The mockWebServer instance.
* @param searchEngineName The name of the new search engine.
*/
private fun createCustomSearchEngine(mockWebServer: MockWebServer, searchEngineName: String): SearchEngine {
val searchString =
"http://localhost:${mockWebServer.port}/pages/searchResults.html?search={searchTerms}"
return createSearchEngine(
name = searchEngineName,
url = searchString,
icon = DefaultIconGenerator().generate(appContext, IconRequest(searchString)).bitmap,
)
}
/**
* Adds a new custom search engine to the apps Search Engines list.
*
* @param searchEngine Use createCustomSearchEngine method to create one.
*/
fun addCustomSearchEngine(mockWebServer: MockWebServer, searchEngineName: String) {
val searchEngine = createCustomSearchEngine(mockWebServer, searchEngineName)
appContext.components.useCases.searchUseCases.addSearchEngine(searchEngine)
}
/**
* Adds and selects as default a new custom search engine to the apps Search Engines list.
*
* @param searchEngine Use createCustomSearchEngine method to create one.
*/
fun setCustomSearchEngine(mockWebServer: MockWebServer, searchEngineName: String) {
val searchEngine = createCustomSearchEngine(mockWebServer, searchEngineName)
with(appContext.components.useCases.searchUseCases) {
addSearchEngine(searchEngine)
selectSearchEngine(searchEngine)
}
}
}

@ -81,6 +81,12 @@ object TestAssetHelper {
return TestAsset(url, "", "")
}
fun getPdfFormAsset(server: MockWebServer): TestAsset {
val url = server.url("resources/pdfForm.pdf").toString().toUri()!!
return TestAsset(url, "", "")
}
fun getSaveLoginAsset(server: MockWebServer): TestAsset {
val url = server.url("pages/password.html").toString().toUri()!!

@ -52,11 +52,13 @@ import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import junit.framework.AssertionFailedError
import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.browser.state.state.availableSearchEngines
import mozilla.components.support.ktx.android.content.appName
import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.Matcher
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.Config
@ -344,13 +346,6 @@ object TestHelper {
fun getStringResource(id: Int, argument: String = appName) = appContext.resources.getString(id, argument)
fun setCustomSearchEngine(searchEngine: SearchEngine) {
with(appContext.components.useCases.searchUseCases) {
addSearchEngine(searchEngine)
selectSearchEngine(searchEngine)
}
}
// Permission allow dialogs differ on various Android APIs
fun grantSystemPermission() {
val whileUsingTheAppPermissionButton: UiObject =
@ -422,7 +417,7 @@ object TestHelper {
/**
* Changes the default language of the entire device, not just the app.
*/
private fun setSystemLocale(locale: Locale) {
fun setSystemLocale(locale: Locale) {
val activityManagerNative = Class.forName("android.app.ActivityManagerNative")
val am = activityManagerNative.getMethod("getDefault", *arrayOfNulls(0))
.invoke(activityManagerNative, *arrayOfNulls(0))
@ -485,4 +480,36 @@ object TestHelper {
mDevice.pressRecentApps()
mDevice.findObject(UiSelector().resourceId("$packageName:id/container")).waitForExists(waitingTime)
}
fun verifyKeyboardVisibility(isExpectedToBeVisible: Boolean = true) {
mDevice.waitForIdle()
assertEquals(
"Keyboard not shown",
isExpectedToBeVisible,
mDevice
.executeShellCommand("dumpsys input_method | grep mInputShown")
.contains("mInputShown=true"),
)
}
/**
* The list of Search engines for the "home" region of the user.
* For en-us it will return the 6 engines selected by default: Google, Bing, DuckDuckGo, Amazon, Ebay, Wikipedia.
*/
fun getRegionSearchEnginesList(): List<SearchEngine> {
val searchEnginesList = appContext.components.core.store.state.search.regionSearchEngines
assertTrue("Search engines list returned nothing", searchEnginesList.isNotEmpty())
return searchEnginesList
}
/**
* The list of Search engines available to be added by user choice.
* For en-us it will return the 2 engines: Reddit, Youtube.
*/
fun getAvailableSearchEngines(): List<SearchEngine> {
val searchEnginesList = appContext.components.core.store.state.search.availableSearchEngines
assertTrue("Search engines list returned nothing", searchEnginesList.isNotEmpty())
return searchEnginesList
}
}

@ -0,0 +1,189 @@
/* 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/. */
@file:Suppress("DEPRECATION")
package org.mozilla.fenix.screenshots
import android.os.SystemClock
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.ui.robots.bookmarksMenu
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.swipeToBottom
import tools.fastlane.screengrab.Screengrab
import tools.fastlane.screengrab.locale.LocaleTestRule
class ComposeMenuScreenShotTest : ScreenshotTest() {
private lateinit var mockWebServer: MockWebServer
private lateinit var mDevice: UiDevice
@Rule
@JvmField
val localeTestRule = LocaleTestRule()
@get:Rule
val composeTestRule =
AndroidComposeTestRule(
HomeActivityTestRule.withDefaultSettingsOverrides(
tabsTrayRewriteEnabled = true,
),
) { it.activity }
@Before
fun setUp() {
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@After
fun tearDown() {
composeTestRule.activity.finishAndRemoveTask()
mockWebServer.shutdown()
}
@Test
fun threeDotMenuTest() {
homeScreen {
}.openThreeDotMenu {
Screengrab.screenshot("ThreeDotMenuMainRobot_three-dot-menu")
}
}
@Test
fun settingsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
Screengrab.screenshot("SettingsRobot_settings-menu")
}.openTurnOnSyncMenu {
Screengrab.screenshot("AccountSettingsRobot_settings-account")
}.goBack {
}.openSearchSubMenu {
Screengrab.screenshot("SettingsSubMenuSearchRobot_settings-search")
}.goBack {
}.openCustomizeSubMenu {
Screengrab.screenshot("SettingsSubMenuThemeRobot_settings-theme")
}.goBack {
}.openAccessibilitySubMenu {
Screengrab.screenshot("SettingsSubMenuAccessibilityRobot_settings-accessibility")
}.goBack {
}.openLanguageSubMenu {
Screengrab.screenshot("SettingsSubMenuAccessibilityRobot_settings-language")
}.goBack {
// From about here we need to scroll up to ensure all settings options are visible.
}.openSetDefaultBrowserSubMenu {
Screengrab.screenshot("SettingsSubMenuDefaultBrowserRobot_settings-default-browser")
}.goBack {
// Disabled for Pixel 2
// }.openEnhancedTrackingProtectionSubMenu {
// Screengrab.screenshot("settings-enhanced-tp")
// }.goBack {
}.openLoginsAndPasswordSubMenu {
Screengrab.screenshot("SettingsSubMenuLoginsAndPasswords-settings-logins-passwords")
}.goBack {
swipeToBottom()
Screengrab.screenshot("SettingsRobot_settings-scroll-to-bottom")
}.openSettingsSubMenuDataCollection {
Screengrab.screenshot("settings-telemetry")
}.goBack {
}.openAddonsManagerMenu {
Screengrab.screenshot("settings-addons")
}
}
@Test
fun historyTest() {
homeScreen {
}.openThreeDotMenu {
}
openHistoryThreeDotMenu()
Screengrab.screenshot("HistoryRobot_history-menu")
}
@Test
fun bookmarksManagementTest() {
homeScreen {
}.openThreeDotMenu {
}
openBookmarksThreeDotMenu()
Screengrab.screenshot("BookmarksRobot_bookmarks-menu")
bookmarksMenu {
clickAddFolderButtonUsingId()
Screengrab.screenshot("BookmarksRobot_add-folder-view")
saveNewFolder()
Screengrab.screenshot("BookmarksRobot_error-empty-folder-name")
addNewFolderName("test")
saveNewFolder()
}.openThreeDotMenu("test") {
Screengrab.screenshot("ThreeDotMenuBookmarksRobot_folder-menu")
}
editBookmarkFolder()
Screengrab.screenshot("ThreeDotMenuBookmarksRobot_edit-bookmark-folder-menu")
// It may be needed to wait here to have the screenshot
bookmarksMenu {
navigateUp()
}.openThreeDotMenu("test") {
deleteBookmarkFolder()
Screengrab.screenshot("ThreeDotMenuBookmarksRobot_delete-bookmark-folder-menu")
}
}
@Test
fun collectionMenuTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
Screengrab.screenshot("NavigationToolbarRobot_navigation-toolbar")
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
Screengrab.screenshot("BrowserRobot_enter-url")
}.openComposeTabDrawer(composeTestRule) {
TestAssetHelper.waitingTime
Screengrab.screenshot("TabDrawerRobot_one-tab-open")
}.openThreeDotMenu {
TestAssetHelper.waitingTime
Screengrab.screenshot("TabDrawerRobot_three-dot-menu")
}
}
@Test
fun tabMenuTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
Screengrab.screenshot("TabDrawerRobot_browser-tab-menu")
}.closeBrowserMenuToBrowser {
}.openComposeTabDrawer(composeTestRule) {
Screengrab.screenshot("TabDrawerRobot_tab-drawer-with-tabs")
closeTab()
TestAssetHelper.waitingTime
Screengrab.screenshot("TabDrawerRobot_remove-tab")
}
}
@Test
fun saveLoginPromptTest() {
val saveLoginTest =
TestAssetHelper.getSaveLoginAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(saveLoginTest.url) {
verifySaveLoginPromptIsShownNotSave()
SystemClock.sleep(TestAssetHelper.waitingTimeShort)
Screengrab.screenshot("save-login-prompt")
}
}
}

@ -16,21 +16,13 @@
]
},
"default": {
"async-generator": {
"hashes": [
"sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b",
"sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"
],
"markers": "python_version >= '3.5'",
"version": "==1.10"
},
"attrs": {
"hashes": [
"sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836",
"sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"
"sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04",
"sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"
],
"markers": "python_version >= '3.6'",
"version": "==22.2.0"
"markers": "python_version >= '3.7'",
"version": "==23.1.0"
},
"blessed": {
"hashes": [
@ -42,11 +34,11 @@
},
"certifi": {
"hashes": [
"sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3",
"sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"
"sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082",
"sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"
],
"markers": "python_version >= '3.6'",
"version": "==2022.12.7"
"index": "pypi",
"version": "==2023.7.22"
},
"cffi": {
"hashes": [
@ -119,40 +111,113 @@
},
"charset-normalizer": {
"hashes": [
"sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845",
"sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"
"sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96",
"sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c",
"sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710",
"sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706",
"sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020",
"sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252",
"sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad",
"sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329",
"sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a",
"sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f",
"sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6",
"sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4",
"sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a",
"sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46",
"sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2",
"sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23",
"sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace",
"sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd",
"sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982",
"sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10",
"sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2",
"sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea",
"sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09",
"sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5",
"sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149",
"sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489",
"sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9",
"sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80",
"sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592",
"sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3",
"sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6",
"sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed",
"sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c",
"sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200",
"sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a",
"sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e",
"sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d",
"sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6",
"sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623",
"sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669",
"sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3",
"sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa",
"sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9",
"sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2",
"sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f",
"sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1",
"sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4",
"sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a",
"sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8",
"sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3",
"sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029",
"sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f",
"sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959",
"sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22",
"sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7",
"sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952",
"sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346",
"sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e",
"sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d",
"sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299",
"sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd",
"sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a",
"sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3",
"sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037",
"sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94",
"sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c",
"sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858",
"sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a",
"sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449",
"sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c",
"sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918",
"sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1",
"sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c",
"sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac",
"sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"
],
"markers": "python_version >= '3.6'",
"version": "==2.1.1"
"markers": "python_version >= '3.7'",
"version": "==3.2.0"
},
"cryptography": {
"hashes": [
"sha256:0f8da300b5c8af9f98111ffd512910bc792b4c77392a9523624680f7956a99d4",
"sha256:35f7c7d015d474f4011e859e93e789c87d21f6f4880ebdc29896a60403328f1f",
"sha256:4789d1e3e257965e960232345002262ede4d094d1a19f4d3b52e48d4d8f3b885",
"sha256:5aa67414fcdfa22cf052e640cb5ddc461924a045cacf325cd164e65312d99502",
"sha256:5d2d8b87a490bfcd407ed9d49093793d0f75198a35e6eb1a923ce1ee86c62b41",
"sha256:6687ef6d0a6497e2b58e7c5b852b53f62142cfa7cd1555795758934da363a965",
"sha256:6f8ba7f0328b79f08bdacc3e4e66fb4d7aab0c3584e0bd41328dce5262e26b2e",
"sha256:706843b48f9a3f9b9911979761c91541e3d90db1ca905fd63fee540a217698bc",
"sha256:807ce09d4434881ca3a7594733669bd834f5b2c6d5c7e36f8c00f691887042ad",
"sha256:83e17b26de248c33f3acffb922748151d71827d6021d98c70e6c1a25ddd78505",
"sha256:96f1157a7c08b5b189b16b47bc9db2332269d6680a196341bf30046330d15388",
"sha256:aec5a6c9864be7df2240c382740fcf3b96928c46604eaa7f3091f58b878c0bb6",
"sha256:b0afd054cd42f3d213bf82c629efb1ee5f22eba35bf0eec88ea9ea7304f511a2",
"sha256:c5caeb8188c24888c90b5108a441c106f7faa4c4c075a2bcae438c6e8ca73cef",
"sha256:ced4e447ae29ca194449a3f1ce132ded8fcab06971ef5f618605aacaa612beac",
"sha256:d1f6198ee6d9148405e49887803907fe8962a23e6c6f83ea7d98f1c0de375695",
"sha256:e124352fd3db36a9d4a21c1aa27fd5d051e621845cb87fb851c08f4f75ce8be6",
"sha256:e422abdec8b5fa8462aa016786680720d78bdce7a30c652b7fadf83a4ba35336",
"sha256:ef8b72fa70b348724ff1218267e7f7375b8de4e8194d1636ee60510aae104cd0",
"sha256:f0c64d1bd842ca2633e74a1a28033d139368ad959872533b1bab8c80e8240a0c",
"sha256:f24077a3b5298a5a06a8e0536e3ea9ec60e4c7ac486755e5fb6e6ea9b3500106",
"sha256:fdd188c8a6ef8769f148f88f859884507b954cc64db6b52f66ef199bb9ad660a",
"sha256:fe913f20024eb2cb2f323e42a64bdf2911bb9738a15dba7d3cce48151034e3a8"
"sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711",
"sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7",
"sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd",
"sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e",
"sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58",
"sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0",
"sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d",
"sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83",
"sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831",
"sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766",
"sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b",
"sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c",
"sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182",
"sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f",
"sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa",
"sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4",
"sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a",
"sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2",
"sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76",
"sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5",
"sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee",
"sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f",
"sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14"
],
"index": "pypi",
"version": "==39.0.1"
"markers": "python_version >= '3.7'",
"version": "==41.0.2"
},
"distro": {
"hashes": [
@ -164,11 +229,11 @@
},
"exceptiongroup": {
"hashes": [
"sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e",
"sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"
"sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5",
"sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"
],
"markers": "python_version < '3.11'",
"version": "==1.1.0"
"version": "==1.1.2"
},
"fxapom": {
"hashes": [
@ -211,10 +276,10 @@
},
"mozdevice": {
"hashes": [
"sha256:8005df3c77bcb50e7b110ce75310ba11aaab44d550dbbe36d30e82f920ea551a",
"sha256:9a55047998fdffc00c0de2587fd818a5083520b60ce662036a2134515d485f84"
"sha256:0f33260e74d734d5f3ac277b28064bac0f5bd1ecce111fe1a73ca61eb3e4b524",
"sha256:ff0e0d4c618a595c26d9d1a9a071db1f5c5383b4c5cb9e7889019bd13885825b"
],
"version": "==4.1.0"
"version": "==4.1.1"
},
"mozdownload": {
"hashes": [
@ -233,9 +298,10 @@
},
"mozinfo": {
"hashes": [
"sha256:4961ebef3c5474b9ca470142f88b5de774a069f4e105663a5152b0ef4659785a"
"sha256:5d2b8a5f1b362692f221e33eb3ff47454a580db1a1384614cdc637b31131b438",
"sha256:90e0cfb377fc2cc3fad023d38c1f6d60a9135400ff5684a04abf79ca5cc3c521"
],
"version": "==1.2.2"
"version": "==1.2.3"
},
"mozinstall": {
"hashes": [
@ -247,17 +313,16 @@
},
"mozlog": {
"hashes": [
"sha256:080c0a7fdf01cc9a3c9dc8dc527ec8adfb447ca0cd05c5f5fe5c944ceeaec3ff",
"sha256:dc389dc861be3fe9ad1db561893a34015935bd203548685000c84ee5177ce05a"
"sha256:26e5e9586afe2d6359a3d75aa6ea25aa2904d0062d0a158418682e44458d98e9"
],
"version": "==7.1.1"
"version": "==8.0.0"
},
"mozprocess": {
"hashes": [
"sha256:e64591d02899f6fd50fe391f5635236c5c604d8faae08ab4aafed9c3a66e3966",
"sha256:e8093fe990bafd4ff3e2e49641b182c8085ea9cee2a28c7bdf9fabc2ff9f618f"
"sha256:7dc38ec3c11693e9944ade1558392c04f37fd8df68d3ec7a20372dfe96b2e5bb",
"sha256:ae343fabb72840195278b73ad426fb04d35d66b15a664f5c47221f4e4f0842e5"
],
"version": "==1.3.0"
"version": "==1.3.1"
},
"mozprofile": {
"hashes": [
@ -300,19 +365,19 @@
},
"packaging": {
"hashes": [
"sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2",
"sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"
"sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61",
"sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"
],
"markers": "python_version >= '3.7'",
"version": "==23.0"
"version": "==23.1"
},
"pluggy": {
"hashes": [
"sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
"sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
"sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849",
"sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"
],
"markers": "python_version >= '3.6'",
"version": "==1.0.0"
"markers": "python_version >= '3.7'",
"version": "==1.2.0"
},
"progressbar2": {
"hashes": [
@ -352,11 +417,11 @@
},
"pyjwt": {
"hashes": [
"sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd",
"sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14"
"sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de",
"sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"
],
"markers": "python_version >= '3.7'",
"version": "==2.6.0"
"version": "==2.8.0"
},
"pypom": {
"hashes": [
@ -407,11 +472,11 @@
},
"python-utils": {
"hashes": [
"sha256:68198854fc276bc4b2403b261703c218e01ef564dcb072a7096ed9ea7aa5130c",
"sha256:8bfefc3430f1c48408fa0e5958eee51d39840a5a987c2181a579e99ab6fe5ca6"
"sha256:1970468fff1c0adbd60b9a751e6a786223a9f0373c954571912d9cf4be49b552",
"sha256:e31c1187168c314c984932e99c2d3f973465443493869ae041dd9e2e18e998aa"
],
"markers": "python_version >= '3.7'",
"version": "==3.5.2"
"markers": "python_version >= '3.9'",
"version": "==3.7.0"
},
"redo": {
"hashes": [
@ -422,27 +487,27 @@
},
"requests": {
"hashes": [
"sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983",
"sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"
"sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
"sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
],
"index": "pypi",
"version": "==2.28.1"
"version": "==2.31.0"
},
"selenium": {
"hashes": [
"sha256:bd04eb41395605d9b2b65fe587f3fed21431da75512985c52772529e5e210c60",
"sha256:c48372905bffcc3b24bd55ab4683a07ee5e1f30fe918c59558ea5ee44cedf6c3"
"sha256:40241b9d872f58959e9b34e258488bf11844cd86142fd68182bd41db9991fc5c",
"sha256:871bf800c4934f745b909c8dfc7d15c65cf45bd2e943abd54451c810ada395e3"
],
"markers": "python_version >= '3.7'",
"version": "==4.8.2"
"version": "==4.10.0"
},
"setuptools": {
"hashes": [
"sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330",
"sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251"
"sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f",
"sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"
],
"markers": "python_version >= '3.7'",
"version": "==67.4.0"
"version": "==68.0.0"
},
"six": {
"hashes": [
@ -484,30 +549,35 @@
},
"trio": {
"hashes": [
"sha256:ce68f1c5400a47b137c5a4de72c7c901bd4e7a24fbdebfe9b41de8c6c04eaacf",
"sha256:f1dd0780a89bfc880c7c7994519cb53f62aacb2c25ff487001c0052bd721cdf0"
"sha256:3887cf18c8bcc894433420305468388dac76932e9668afa1c49aa3806b6accb3",
"sha256:f43da357620e5872b3d940a2e3589aa251fd3f881b65a608d742e00809b1ec38"
],
"markers": "python_version >= '3.7'",
"version": "==0.22.0"
"version": "==0.22.2"
},
"trio-websocket": {
"hashes": [
"sha256:5b558f6e83cc20a37c3b61202476c5295d1addf57bd65543364e0337e37ed2bc",
"sha256:a3d34de8fac26023eee701ed1e7bf4da9a8326b61a62934ec9e53b64970fd8fe"
"sha256:1a748604ad906a7dcab9a43c6eb5681e37de4793ba0847ef0bc9486933ed027b",
"sha256:a9937d48e8132ebf833019efde2a52ca82d223a30a7ea3e8d60a7d28f75a4e3a"
],
"markers": "python_version >= '3.5'",
"version": "==0.9.2"
"markers": "python_version >= '3.7'",
"version": "==0.10.3"
},
"urllib3": {
"extras": [
"typing-extensions": {
"hashes": [
"sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36",
"sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"
],
"markers": "python_version >= '3.7'",
"version": "==4.7.1"
},
"urllib3": {
"hashes": [
"sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72",
"sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"
"sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11",
"sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==1.26.14"
"markers": "python_version >= '3.7'",
"version": "==2.0.4"
},
"wcwidth": {
"hashes": [
@ -534,18 +604,19 @@
},
"zope.component": {
"hashes": [
"sha256:a508f9fef1b6f5286462d3340cd89ffab5c7899dca0401337239cb6ba7c6bb0a",
"sha256:cbd279e15a959f35a813b64ec4f1027c08b86fcca26f73250c0c912251df90dd"
"sha256:96d0a04db39643caf2dfaec152340f3e914df1dc3fa32fbb913782620dc6c3c6",
"sha256:9a0a0472ad201b94b4fe6741ce9ac2c30b8bb22c516077bf03692dec4dfb6906"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==5.1.0"
"markers": "python_version >= '3.7'",
"version": "==6.0"
},
"zope.event": {
"hashes": [
"sha256:73d9e3ef750cca14816a9c322c7250b0d7c9dbc337df5d1b807ff8d3d0b9e97c",
"sha256:81d98813046fc86cc4136e3698fee628a3282f9c320db18658c21749235fce80"
"sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26",
"sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd"
],
"version": "==4.6"
"markers": "python_version >= '3.7'",
"version": "==5.0"
},
"zope.hookable": {
"hashes": [
@ -591,45 +662,39 @@
},
"zope.interface": {
"hashes": [
"sha256:008b0b65c05993bb08912f644d140530e775cf1c62a072bf9340c2249e613c32",
"sha256:0217a9615531c83aeedb12e126611b1b1a3175013bbafe57c702ce40000eb9a0",
"sha256:0fb497c6b088818e3395e302e426850f8236d8d9f4ef5b2836feae812a8f699c",
"sha256:17ebf6e0b1d07ed009738016abf0d0a0f80388e009d0ac6e0ead26fc162b3b9c",
"sha256:311196634bb9333aa06f00fc94f59d3a9fddd2305c2c425d86e406ddc6f2260d",
"sha256:3218ab1a7748327e08ef83cca63eea7cf20ea7e2ebcb2522072896e5e2fceedf",
"sha256:404d1e284eda9e233c90128697c71acffd55e183d70628aa0bbb0e7a3084ed8b",
"sha256:4087e253bd3bbbc3e615ecd0b6dd03c4e6a1e46d152d3be6d2ad08fbad742dcc",
"sha256:40f4065745e2c2fa0dff0e7ccd7c166a8ac9748974f960cd39f63d2c19f9231f",
"sha256:5334e2ef60d3d9439c08baedaf8b84dc9bb9522d0dacbc10572ef5609ef8db6d",
"sha256:604cdba8f1983d0ab78edc29aa71c8df0ada06fb147cea436dc37093a0100a4e",
"sha256:6373d7eb813a143cb7795d3e42bd8ed857c82a90571567e681e1b3841a390d16",
"sha256:655796a906fa3ca67273011c9805c1e1baa047781fca80feeb710328cdbed87f",
"sha256:65c3c06afee96c654e590e046c4a24559e65b0a87dbff256cd4bd6f77e1a33f9",
"sha256:696f3d5493eae7359887da55c2afa05acc3db5fc625c49529e84bd9992313296",
"sha256:6e972493cdfe4ad0411fd9abfab7d4d800a7317a93928217f1a5de2bb0f0d87a",
"sha256:7579960be23d1fddecb53898035a0d112ac858c3554018ce615cefc03024e46d",
"sha256:765d703096ca47aa5d93044bf701b00bbce4d903a95b41fff7c3796e747b1f1d",
"sha256:7e66f60b0067a10dd289b29dceabd3d0e6d68be1504fc9d0bc209cf07f56d189",
"sha256:8a2ffadefd0e7206adc86e492ccc60395f7edb5680adedf17a7ee4205c530df4",
"sha256:959697ef2757406bff71467a09d940ca364e724c534efbf3786e86eee8591452",
"sha256:9d783213fab61832dbb10d385a319cb0e45451088abd45f95b5bb88ed0acca1a",
"sha256:a16025df73d24795a0bde05504911d306307c24a64187752685ff6ea23897cb0",
"sha256:a2ad597c8c9e038a5912ac3cf166f82926feff2f6e0dabdab956768de0a258f5",
"sha256:bfee1f3ff62143819499e348f5b8a7f3aa0259f9aca5e0ddae7391d059dce671",
"sha256:d169ccd0756c15bbb2f1acc012f5aab279dffc334d733ca0d9362c5beaebe88e",
"sha256:d514c269d1f9f5cd05ddfed15298d6c418129f3f064765295659798349c43e6f",
"sha256:d692374b578360d36568dd05efb8a5a67ab6d1878c29c582e37ddba80e66c396",
"sha256:dbaeb9cf0ea0b3bc4b36fae54a016933d64c6d52a94810a63c00f440ecb37dd7",
"sha256:dc26c8d44472e035d59d6f1177eb712888447f5799743da9c398b0339ed90b1b",
"sha256:e1574980b48c8c74f83578d1e77e701f8439a5d93f36a5a0af31337467c08fcf",
"sha256:e74a578172525c20d7223eac5f8ad187f10940dac06e40113d62f14f3adb1e8f",
"sha256:e945de62917acbf853ab968d8916290548df18dd62c739d862f359ecd25842a6",
"sha256:f0980d44b8aded808bec5059018d64692f0127f10510eca71f2f0ace8fb11188",
"sha256:f98d4bd7bbb15ca701d19b93263cc5edfd480c3475d163f137385f49e5b3a3a7",
"sha256:fb68d212efd057596dee9e6582daded9f8ef776538afdf5feceb3059df2d2e7b"
"sha256:042f2381118b093714081fd82c98e3b189b68db38ee7d35b63c327c470ef8373",
"sha256:0ec9653825f837fbddc4e4b603d90269b501486c11800d7c761eee7ce46d1bbb",
"sha256:12175ca6b4db7621aedd7c30aa7cfa0a2d65ea3a0105393e05482d7a2d367446",
"sha256:1592f68ae11e557b9ff2bc96ac8fc30b187e77c45a3c9cd876e3368c53dc5ba8",
"sha256:23ac41d52fd15dd8be77e3257bc51bbb82469cf7f5e9a30b75e903e21439d16c",
"sha256:424d23b97fa1542d7be882eae0c0fc3d6827784105264a8169a26ce16db260d8",
"sha256:4407b1435572e3e1610797c9203ad2753666c62883b921318c5403fb7139dec2",
"sha256:48f4d38cf4b462e75fac78b6f11ad47b06b1c568eb59896db5b6ec1094eb467f",
"sha256:4c3d7dfd897a588ec27e391edbe3dd320a03684457470415870254e714126b1f",
"sha256:5171eb073474a5038321409a630904fd61f12dd1856dd7e9d19cd6fe092cbbc5",
"sha256:5a158846d0fca0a908c1afb281ddba88744d403f2550dc34405c3691769cdd85",
"sha256:6ee934f023f875ec2cfd2b05a937bd817efcc6c4c3f55c5778cbf78e58362ddc",
"sha256:790c1d9d8f9c92819c31ea660cd43c3d5451df1df61e2e814a6f99cebb292788",
"sha256:809fe3bf1a91393abc7e92d607976bbb8586512913a79f2bf7d7ec15bd8ea518",
"sha256:87b690bbee9876163210fd3f500ee59f5803e4a6607d1b1238833b8885ebd410",
"sha256:89086c9d3490a0f265a3c4b794037a84541ff5ffa28bb9c24cc9f66566968464",
"sha256:99856d6c98a326abbcc2363827e16bd6044f70f2ef42f453c0bd5440c4ce24e5",
"sha256:aab584725afd10c710b8f1e6e208dbee2d0ad009f57d674cb9d1b3964037275d",
"sha256:af169ba897692e9cd984a81cb0f02e46dacdc07d6cf9fd5c91e81f8efaf93d52",
"sha256:b39b8711578dcfd45fc0140993403b8a81e879ec25d53189f3faa1f006087dca",
"sha256:b3f543ae9d3408549a9900720f18c0194ac0fe810cecda2a584fd4dca2eb3bb8",
"sha256:d0583b75f2e70ec93f100931660328965bb9ff65ae54695fb3fa0a1255daa6f2",
"sha256:dfbbbf0809a3606046a41f8561c3eada9db811be94138f42d9135a5c47e75f6f",
"sha256:e538f2d4a6ffb6edfb303ce70ae7e88629ac6e5581870e66c306d9ad7b564a58",
"sha256:eba51599370c87088d8882ab74f637de0c4f04a6d08a312dce49368ba9ed5c2a",
"sha256:ee4b43f35f5dc15e1fec55ccb53c130adb1d11e8ad8263d68b1284b66a04190d",
"sha256:f2363e5fd81afb650085c6686f2ee3706975c54f331b426800b53531191fdf28",
"sha256:f299c020c6679cb389814a3b81200fe55d428012c5e76da7e722491f5d205990",
"sha256:f72f23bab1848edb7472309e9898603141644faec9fd57a823ea6b4d1c4c8995",
"sha256:fa90bac61c9dc3e1a563e5babb3fd2c0c1c80567e815442ddbe561eadc803b30"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==5.5.2"
"markers": "python_version >= '3.7'",
"version": "==6.0"
}
},
"develop": {}

@ -9,7 +9,7 @@ logging.getLogger(__name__).addHandler(logging.NullHandler())
class GradlewBuild(object):
binary = './gradlew'
binary = "./gradlew"
logger = logging.getLogger()
adbrun = ADBrun()
@ -20,17 +20,19 @@ class GradlewBuild(object):
self.adbrun.launch()
# Change path accordingly to go to root folder to run gradlew
os.chdir('../../../../../../../..')
cmd = './gradlew ' + 'app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=org.mozilla.fenix.syncintegration.SyncIntegrationTest#{}'.format(identifier)
os.chdir("../../../../../../../..")
cmd = (
"./gradlew "
+ "app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=org.mozilla.fenix.syncintegration.SyncIntegrationTest#{}".format(
identifier
)
)
self.logger.info('Running cmd: {}'.format(cmd))
self.logger.info("Running cmd: {}".format(cmd))
out = ""
try:
out = subprocess.check_output(
cmd,
shell=True,
stderr=subprocess.STDOUT)
out = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
out = e.output
raise
@ -39,5 +41,5 @@ class GradlewBuild(object):
testsPath = "app/src/androidTest/java/org/mozilla/fenix/syncintegration/"
os.chdir(testsPath)
with open(self.log, 'w') as f:
with open(self.log, "w") as f:
f.write(str(out))

@ -13,39 +13,42 @@ class TPS(object):
def __init__(self, firefox, firefox_log, tps_log, profile):
self.firefox = firefox
self.firefox_log = open(firefox_log, 'w')
self.firefox_log = open(firefox_log, "w")
self.tps_log = tps_log
self.profile = profile
def _log(self, line):
self.firefox_log.write(line + '\n')
def run(self, test, phase='phase1', ignore_unused_engines=True):
self.profile.set_preferences({
'testing.tps.testFile': os.path.abspath(test),
'testing.tps.testPhase': phase,
'testing.tps.ignoreUnusedEngines': ignore_unused_engines,
})
args = ['-marionette']
process_args = {'processOutputLine': [self._log]}
self.logger.info('Running: {} {}'.format(self.firefox, ' '.join(args)))
self.logger.info('Using profile at: {}'.format(self.profile.profile))
self.firefox_log.write(line + "\n")
def run(self, test, phase="phase1", ignore_unused_engines=True):
self.profile.set_preferences(
{
"testing.tps.testFile": os.path.abspath(test),
"testing.tps.testPhase": phase,
"testing.tps.ignoreUnusedEngines": ignore_unused_engines,
}
)
args = ["-marionette"]
process_args = {"processOutputLine": [self._log]}
self.logger.info("Running: {} {}".format(self.firefox, " ".join(args)))
self.logger.info("Using profile at: {}".format(self.profile.profile))
runner = FirefoxRunner(
binary=self.firefox,
cmdargs=args,
profile=self.profile,
process_args=process_args)
process_args=process_args,
)
runner.start(timeout=TIMEOUT)
runner.wait(timeout=TIMEOUT)
self.firefox_log.close()
with open(self.tps_log) as f:
for line in f.readlines():
if 'CROSSWEAVE ERROR: ' in line:
raise TPSError(line.partition('CROSSWEAVE ERROR: ')[-1])
if "CROSSWEAVE ERROR: " in line:
raise TPSError(line.partition("CROSSWEAVE ERROR: ")[-1])
with open(self.tps_log) as f:
assert 'test phase {}: PASS'.format(phase) in f.read()
assert "test phase {}: PASS".format(phase) in f.read()
class TPSError(Exception):

@ -50,7 +50,7 @@ class BookmarksTest {
@get:Rule(order = 0)
val activityTestRule =
AndroidComposeTestRule(
HomeActivityIntentTestRule.withDefaultSettingsOverrides(isUnifiedSearchEnabled = true),
HomeActivityIntentTestRule.withDefaultSettingsOverrides(),
) { it.activity }
@Rule(order = 1)

@ -353,6 +353,31 @@ class CollectionTest {
}
}
@Test
fun undoTabRemovalFromCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openTabDrawer {
createCollection(webPage.title, collectionName = collectionName)
closeTab()
}
homeScreen {
verifyCollectionIsDisplayed(collectionName)
}.expandCollection(collectionName) {
verifyTabSavedInCollection(webPage.title, true)
removeTabFromCollection(webPage.title)
}
homeScreen {
verifySnackBarText("Collection deleted")
clickSnackbarButton("UNDO")
verifyCollectionIsDisplayed(collectionName, true)
verifyCollectionIsDisplayed(collectionName, true)
}
}
@Test
fun swipeLeftToRemoveTabFromCollectionTest() {
val testPage = getGenericAsset(mockWebServer, 1)
@ -375,6 +400,8 @@ class CollectionTest {
verifyTabSavedInCollection(testPage.title, false)
}
homeScreen {
verifySnackBarText("Collection deleted")
verifySnackBarText("UNDO")
verifyCollectionIsDisplayed(collectionName, false)
}
}
@ -401,6 +428,8 @@ class CollectionTest {
verifyTabSavedInCollection(testPage.title, false)
}
homeScreen {
verifySnackBarText("Collection deleted")
verifySnackBarText("UNDO")
verifyCollectionIsDisplayed(collectionName, false)
}
}

@ -0,0 +1,872 @@
/* 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.ui
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.Espresso.pressBack
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.uiautomator.UiDevice
import kotlinx.coroutines.runBlocking
import mozilla.appservices.places.BookmarkRoot
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.ext.bookmarkStorage
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
import org.mozilla.fenix.helpers.RetryTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton
import org.mozilla.fenix.helpers.TestHelper.exitMenu
import org.mozilla.fenix.helpers.TestHelper.longTapSelectItem
import org.mozilla.fenix.helpers.TestHelper.registerAndCleanupIdlingResources
import org.mozilla.fenix.ui.robots.bookmarksMenu
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.multipleSelectionToolbar
import org.mozilla.fenix.ui.robots.navigationToolbar
/**
* Tests for verifying basic functionality of bookmarks
*/
class ComposeBookmarksTest {
private lateinit var mockWebServer: MockWebServer
private lateinit var mDevice: UiDevice
private val bookmarksFolderName = "New Folder"
private val testBookmark = object {
var title: String = "Bookmark title"
var url: String = "https://www.example.com"
}
@get:Rule(order = 0)
val activityTestRule =
AndroidComposeTestRule(
HomeActivityIntentTestRule.withDefaultSettingsOverrides(
tabsTrayRewriteEnabled = true,
),
) { it.activity }
@Rule(order = 1)
@JvmField
val retryTestRule = RetryTestRule(3)
@Before
fun setUp() {
mDevice = UiDevice.getInstance(getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@After
fun tearDown() {
mockWebServer.shutdown()
// Clearing all bookmarks data after each test to avoid overlapping data
val bookmarksStorage = activityTestRule.activity?.bookmarkStorage
runBlocking {
val bookmarks = bookmarksStorage?.getTree(BookmarkRoot.Mobile.id)?.children
bookmarks?.forEach { bookmarksStorage.deleteNode(it.guid) }
}
}
@Test
fun verifyEmptyBookmarksMenuTest() {
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1),
) {
verifyBookmarksMenuView()
verifyAddFolderButton()
verifyCloseButton()
verifyBookmarkTitle("Desktop Bookmarks")
}
}
}
@Test
fun defaultDesktopBookmarksFoldersTest() {
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1),
) {
selectFolder("Desktop Bookmarks")
verifyFolderTitle("Bookmarks Menu")
verifyFolderTitle("Bookmarks Toolbar")
verifyFolderTitle("Other Bookmarks")
verifySyncSignInButton()
}
}.clickSingInToSyncButton {
verifyTurnOnSyncToolbarTitle()
}
}
@Test
fun verifyBookmarkButtonTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
}.bookmarkPage {
}.openThreeDotMenu {
verifyEditBookmarkButton()
}
}
@Test
fun addBookmarkTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
) {
verifyBookmarkedURL(defaultWebPage.url.toString())
verifyBookmarkFavicon(defaultWebPage.url)
}
}
}
@Test
fun createBookmarkFolderTest() {
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1),
) {
clickAddFolderButton()
verifyKeyboardVisible()
addNewFolderName(bookmarksFolderName)
saveNewFolder()
verifyFolderTitle(bookmarksFolderName)
verifyKeyboardHidden()
}
}
}
@Test
fun cancelCreateBookmarkFolderTest() {
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
clickAddFolderButton()
addNewFolderName(bookmarksFolderName)
navigateUp()
verifyKeyboardHidden()
verifyBookmarkFolderIsNotCreated(bookmarksFolderName)
}
}
@SmokeTest
@Test
fun cancelEditBookmarkTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
}.bookmarkPage {
clickSnackbarButton("EDIT")
}
bookmarksMenu {
verifyEditBookmarksView()
changeBookmarkTitle(testBookmark.title)
changeBookmarkUrl(testBookmark.url)
}.closeEditBookmarkSection {
}
browserScreen {
}.openThreeDotMenu {
}.openBookmarks {
verifyBookmarkTitle(defaultWebPage.title)
verifyBookmarkedURL(defaultWebPage.url.toString())
}
}
@SmokeTest
@Test
fun editBookmarkTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.editBookmarkPage {
verifyEditBookmarksView()
changeBookmarkTitle(testBookmark.title)
changeBookmarkUrl(testBookmark.url)
saveEditBookmark()
}
browserScreen {
}.openThreeDotMenu {
}.openBookmarks {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
) {}
verifyBookmarkTitle(testBookmark.title)
verifyBookmarkedURL(testBookmark.url)
}.openBookmarkWithTitle(testBookmark.title) {
verifyUrl("example.com")
}
}
@Test
fun copyBookmarkURLTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
) {}
}.openThreeDotMenu(defaultWebPage.title) {
}.clickCopy {
verifyCopySnackBarText()
navigateUp()
}
navigationToolbar {
}.clickUrlbar {
clickClearButton()
longClickToolbar()
clickPasteText()
verifyTypedToolbarText(defaultWebPage.url.toString())
}
}
@Test
fun threeDotMenuShareBookmarkTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
) {}
}.openThreeDotMenu(defaultWebPage.title) {
}.clickShare {
verifyShareOverlay()
verifyShareBookmarkFavicon()
verifyShareBookmarkTitle()
verifyShareBookmarkUrl()
}
}
@Test
fun openBookmarkInNewTabTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
) {}
}.openThreeDotMenu(defaultWebPage.title) {
}.clickOpenInNewTab(activityTestRule) {
verifyTabTrayIsOpen()
verifyNormalBrowsingButtonIsSelected()
}
}
@Test
fun openAllInTabsTest() {
val webPages = listOf(
TestAssetHelper.getGenericAsset(mockWebServer, 1),
TestAssetHelper.getGenericAsset(mockWebServer, 2),
TestAssetHelper.getGenericAsset(mockWebServer, 3),
TestAssetHelper.getGenericAsset(mockWebServer, 4),
)
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
createFolder("root")
createFolder("sub", "root")
createFolder("empty", "root")
}.closeMenu {
}
browserScreen {
createBookmark(webPages[0].url)
createBookmark(webPages[1].url, "root")
createBookmark(webPages[2].url, "root")
createBookmark(webPages[3].url, "sub")
}.openComposeTabDrawer(activityTestRule) {
closeTab()
}
browserScreen {
}.openThreeDotMenu {
}.openBookmarks {
}.openThreeDotMenu("root") {
}.clickOpenAllInTabs(activityTestRule) {
verifyTabTrayIsOpen()
verifyNormalBrowsingButtonIsSelected()
verifyExistingOpenTabs("Test_Page_2", "Test_Page_3", "Test_Page_4")
// Bookmark that is not under the root folder should not be opened
verifyNoExistingOpenTabs("Test_Page_1")
}
}
@Test
fun openAllInPrivateTabsTest() {
val webPages = listOf(
TestAssetHelper.getGenericAsset(mockWebServer, 1),
TestAssetHelper.getGenericAsset(mockWebServer, 2),
)
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
createFolder("root")
createFolder("sub", "root")
createFolder("empty", "root")
}.closeMenu {
}
browserScreen {
createBookmark(webPages[0].url, "root")
createBookmark(webPages[1].url, "sub")
}.openComposeTabDrawer(activityTestRule) {
closeTab()
}
browserScreen {
}.openThreeDotMenu {
}.openBookmarks {
}.openThreeDotMenu("root") {
}.clickOpenAllInPrivateTabs(activityTestRule) {
verifyTabTrayIsOpen()
verifyPrivateBrowsingButtonIsSelected()
verifyExistingOpenTabs("Test_Page_1", "Test_Page_2")
}
}
@Test
fun openBookmarkInPrivateTabTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
) {}
}.openThreeDotMenu(defaultWebPage.title) {
}.clickOpenInPrivateTab(activityTestRule) {
verifyTabTrayIsOpen()
verifyPrivateBrowsingButtonIsSelected()
}
}
@SmokeTest
@Test
fun deleteBookmarkTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
) {}
}.openThreeDotMenu(defaultWebPage.title) {
}.clickDelete {
verifyDeleteSnackBarText()
verifyUndoDeleteSnackBarButton()
}
}
@SmokeTest
@Test
fun undoDeleteBookmarkTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
) {}
}.openThreeDotMenu(defaultWebPage.title) {
}.clickDelete {
verifyUndoDeleteSnackBarButton()
clickUndoDeleteButton()
verifySnackBarHidden()
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
) {
verifyBookmarkedURL(defaultWebPage.url.toString())
}
}
}
@SmokeTest
@Test
fun bookmarksMultiSelectionToolbarItemsTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
) {
longTapSelectItem(defaultWebPage.url)
}
}
multipleSelectionToolbar {
verifyMultiSelectionCheckmark(defaultWebPage.url)
verifyMultiSelectionCounter()
verifyShareBookmarksButton()
verifyCloseToolbarButton()
}.closeToolbarReturnToBookmarks {
verifyBookmarksMenuView()
}
}
@SmokeTest
@Test
fun openSelectionInNewTabTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openComposeTabDrawer(activityTestRule) {
closeTab()
}
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
) {
longTapSelectItem(defaultWebPage.url)
openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
}
}
multipleSelectionToolbar {
}.clickOpenNewTab(activityTestRule) {
verifyTabTrayIsOpen()
verifyNormalBrowsingButtonIsSelected()
verifyNormalTabsList()
}
}
@SmokeTest
@Test
fun openSelectionInPrivateTabTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
) {
longTapSelectItem(defaultWebPage.url)
openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
}
}
multipleSelectionToolbar {
}.clickOpenPrivateTab(activityTestRule) {
verifyPrivateBrowsingButtonIsSelected()
verifyPrivateTabsList()
}
}
@SmokeTest
@Test
fun deleteMultipleSelectionTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
browserScreen {
createBookmark(firstWebPage.url)
createBookmark(secondWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 3),
) {
longTapSelectItem(firstWebPage.url)
longTapSelectItem(secondWebPage.url)
}
openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
}
multipleSelectionToolbar {
clickMultiSelectionDelete()
}
bookmarksMenu {
verifyDeleteMultipleBookmarksSnackBar()
}
}
@SmokeTest
@Test
fun undoDeleteMultipleSelectionTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
browserScreen {
createBookmark(firstWebPage.url)
createBookmark(secondWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 3),
) {
longTapSelectItem(firstWebPage.url)
longTapSelectItem(secondWebPage.url)
}
openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
}
multipleSelectionToolbar {
clickMultiSelectionDelete()
}
bookmarksMenu {
verifyDeleteMultipleBookmarksSnackBar()
clickUndoDeleteButton()
verifyBookmarkedURL(firstWebPage.url.toString())
verifyBookmarkedURL(secondWebPage.url.toString())
}
}
@Test
fun multipleSelectionShareButtonTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
) {
longTapSelectItem(defaultWebPage.url)
}
}
multipleSelectionToolbar {
clickShareBookmarksButton()
verifyShareOverlay()
verifyShareTabFavicon()
verifyShareTabTitle()
verifyShareTabUrl()
}
}
@Test
fun multipleBookmarkDeletionsTest() {
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
createFolder("1")
getInstrumentation().waitForIdleSync()
createFolder("2")
getInstrumentation().waitForIdleSync()
createFolder("3")
getInstrumentation().waitForIdleSync()
}.openThreeDotMenu("1") {
}.clickDelete {
verifyDeleteFolderConfirmationMessage()
confirmDeletion()
verifyDeleteSnackBarText()
}.openThreeDotMenu("2") {
}.clickDelete {
verifyDeleteFolderConfirmationMessage()
confirmDeletion()
verifyDeleteSnackBarText()
verifyFolderTitle("3")
// On some devices we need to wait for the Snackbar to be gone before continuing
TestHelper.waitUntilSnackbarGone()
}.closeMenu {
}
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
verifyFolderTitle("3")
}
}
@SmokeTest
@Test
fun changeBookmarkParentFolderTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
) {
createFolder(bookmarksFolderName)
}
}.openThreeDotMenu(defaultWebPage.title) {
}.clickEdit {
clickParentFolderSelector()
selectFolder(bookmarksFolderName)
navigateUp()
saveEditBookmark()
selectFolder(bookmarksFolderName)
verifyBookmarkedURL(defaultWebPage.url.toString())
}
}
@Test
fun navigateBookmarksFoldersTest() {
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
createFolder("1")
getInstrumentation().waitForIdleSync()
waitForBookmarksFolderContentToExist("Bookmarks", "1")
selectFolder("1")
verifyCurrentFolderTitle("1")
createFolder("2")
getInstrumentation().waitForIdleSync()
waitForBookmarksFolderContentToExist("1", "2")
selectFolder("2")
verifyCurrentFolderTitle("2")
navigateUp()
waitForBookmarksFolderContentToExist("1", "2")
verifyCurrentFolderTitle("1")
mDevice.pressBack()
verifyBookmarksMenuView()
}
}
@Test
fun cantSelectDesktopFoldersTest() {
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)),
) {
longTapDesktopFolder("Desktop Bookmarks")
verifySelectDefaultFolderSnackBarText()
}
}
}
@Test
fun verifyCloseMenuTest() {
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
}.closeMenu {
verifyHomeScreen()
}
}
@Test
fun deleteBookmarkInEditModeTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
) {}
}.openThreeDotMenu(defaultWebPage.title) {
}.clickEdit {
clickDeleteInEditModeButton()
cancelDeletion()
clickDeleteInEditModeButton()
confirmDeletion()
verifyDeleteSnackBarText()
verifyBookmarkIsDeleted("Test_Page_1")
}
}
@SmokeTest
@Test
fun undoDeleteBookmarkFolderTest() {
browserScreen {
}.openThreeDotMenu {
}.openBookmarks {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1),
) {
createFolder("My Folder")
verifyFolderTitle("My Folder")
}
}.openThreeDotMenu("My Folder") {
}.clickDelete {
cancelFolderDeletion()
verifyFolderTitle("My Folder")
}.openThreeDotMenu("My Folder") {
}.clickDelete {
confirmDeletion()
verifyDeleteSnackBarText()
clickUndoDeleteButton()
verifyFolderTitle("My Folder")
}
}
@Test
fun verifySearchBookmarksViewTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
}.clickSearchButton {
verifySearchView()
verifySearchToolbar(true)
verifySearchSelectorButton()
verifySearchEngineIcon("Bookmarks")
verifySearchBarPlaceholder("Search bookmarks")
verifySearchBarPosition(true)
tapOutsideToDismissSearchBar()
verifySearchToolbar(false)
}
bookmarksMenu {
}.goBackToBrowserScreen {
}.openThreeDotMenu {
}.openSettings {
}.openCustomizeSubMenu {
clickTopToolbarToggle()
}
exitMenu()
browserScreen {
}.openThreeDotMenu {
}.openBookmarks {
}.clickSearchButton {
verifySearchToolbar(true)
verifySearchEngineIcon("Bookmarks")
verifySearchBarPosition(false)
pressBack()
verifySearchToolbar(false)
}
}
@Test
fun verifySearchForBookmarkedItemsTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getHTMLControlsFormAsset(mockWebServer)
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
createFolder(bookmarksFolderName)
}
exitMenu()
browserScreen {
createBookmark(firstWebPage.url, bookmarksFolderName)
createBookmark(secondWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
}.clickSearchButton {
// Search for a valid term
typeSearch(firstWebPage.title)
verifySearchEngineSuggestionResults(activityTestRule, firstWebPage.url.toString())
verifyNoSuggestionsAreDisplayed(activityTestRule, secondWebPage.url.toString())
// Search for invalid term
typeSearch("Android")
verifyNoSuggestionsAreDisplayed(activityTestRule, firstWebPage.url.toString())
verifyNoSuggestionsAreDisplayed(activityTestRule, secondWebPage.url.toString())
}
}
@Test
fun verifyVoiceSearchInBookmarksTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
}.clickSearchButton {
verifySearchToolbar(true)
verifySearchEngineIcon("Bookmarks")
startVoiceSearch()
}
}
@Test
fun verifyDeletedBookmarksCanNotBeSearchedTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
val thirdWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 3)
browserScreen {
createBookmark(firstWebPage.url)
createBookmark(secondWebPage.url)
createBookmark(thirdWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
}.openThreeDotMenu(firstWebPage.title) {
}.clickDelete {
verifyBookmarkIsDeleted(firstWebPage.title)
}.openThreeDotMenu(secondWebPage.title) {
}.clickDelete {
verifyBookmarkIsDeleted(secondWebPage.title)
}.clickSearchButton {
// Search for a valid term
typeSearch("generic")
verifyNoSuggestionsAreDisplayed(activityTestRule, firstWebPage.url.toString())
verifyNoSuggestionsAreDisplayed(activityTestRule, secondWebPage.url.toString())
verifySearchEngineSuggestionResults(activityTestRule, thirdWebPage.url.toString())
pressBack()
}
bookmarksMenu {
}.openThreeDotMenu(thirdWebPage.title) {
}.clickDelete {
verifyBookmarkIsDeleted(thirdWebPage.title)
}.clickSearchButton {
// Search for a valid term
typeSearch("generic")
verifyNoSuggestionsAreDisplayed(activityTestRule, thirdWebPage.url.toString())
}
}
}

@ -0,0 +1,530 @@
/* 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.ui
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton
import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.collectionRobot
import org.mozilla.fenix.ui.robots.composeTabDrawer
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
/**
* Tests for verifying basic functionality of tab collections
*
*/
class ComposeCollectionTest {
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer
private val firstCollectionName = "testcollection_1"
private val secondCollectionName = "testcollection_2"
private val collectionName = "First Collection"
@get:Rule
val composeTestRule =
AndroidComposeTestRule(
HomeActivityIntentTestRule(
isHomeOnboardingDialogEnabled = false,
isJumpBackInCFREnabled = false,
isRecentTabsFeatureEnabled = false,
isRecentlyVisitedFeatureEnabled = false,
isPocketEnabled = false,
isWallpaperOnboardingEnabled = false,
isTCPCFREnabled = false,
tabsTrayRewriteEnabled = true,
),
) { it.activity }
@Before
fun setUp() {
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
@SmokeTest
@Test
fun createFirstCollectionTest() {
val firstWebPage = getGenericAsset(mockWebServer, 1)
val secondWebPage = getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
mDevice.waitForIdle()
}.openComposeTabDrawer(composeTestRule) {
}.openNewTab {
}.submitQuery(secondWebPage.url.toString()) {
mDevice.waitForIdle()
}.goToHomescreen {
}.clickSaveTabsToCollectionButton(composeTestRule) {
longClickTab(firstWebPage.title)
selectTab(secondWebPage.title)
verifyTabsMultiSelectionCounter(2)
}.clickSaveCollection {
typeCollectionNameAndSave(collectionName)
}
composeTabDrawer(composeTestRule) {
verifySnackBarText("Collection saved!")
clickSnackbarButton("VIEW")
}
homeScreen {
verifyCollectionIsDisplayed(collectionName)
}
}
@SmokeTest
@Test
fun verifyExpandedCollectionItemsTest() {
val webPage = getGenericAsset(mockWebServer, 1)
val webPageUrl = webPage.url.host.toString()
navigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openComposeTabDrawer(composeTestRule) {
createCollection(webPage.title, collectionName = collectionName)
clickSnackbarButton("VIEW")
}
homeScreen {
verifyCollectionIsDisplayed(collectionName)
}.expandCollection(collectionName) {
verifyTabSavedInCollection(webPage.title)
verifyCollectionTabUrl(true, webPageUrl)
verifyShareCollectionButtonIsVisible(true)
verifyCollectionMenuIsVisible(true, composeTestRule)
verifyCollectionItemRemoveButtonIsVisible(webPage.title, true)
}.collapseCollection(collectionName) {}
collectionRobot {
verifyTabSavedInCollection(webPage.title, false)
verifyShareCollectionButtonIsVisible(false)
verifyCollectionMenuIsVisible(false, composeTestRule)
verifyCollectionTabUrl(false, webPageUrl)
verifyCollectionItemRemoveButtonIsVisible(webPage.title, false)
}
homeScreen {
verifyCollectionIsDisplayed(collectionName)
}.expandCollection(collectionName) {
verifyTabSavedInCollection(webPage.title)
verifyCollectionTabUrl(true, webPageUrl)
verifyShareCollectionButtonIsVisible(true)
verifyCollectionMenuIsVisible(true, composeTestRule)
verifyCollectionItemRemoveButtonIsVisible(webPage.title, true)
}.collapseCollection(collectionName) {}
collectionRobot {
verifyTabSavedInCollection(webPage.title, false)
verifyShareCollectionButtonIsVisible(false)
verifyCollectionMenuIsVisible(false, composeTestRule)
verifyCollectionTabUrl(false, webPageUrl)
verifyCollectionItemRemoveButtonIsVisible(webPage.title, false)
}
}
@SmokeTest
@Test
fun openAllTabsInCollectionTest() {
val firstTestPage = getGenericAsset(mockWebServer, 1)
val secondTestPage = getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstTestPage.url) {
waitForPageToLoad()
}.openComposeTabDrawer(composeTestRule) {
}.openNewTab {
}.submitQuery(secondTestPage.url.toString()) {
waitForPageToLoad()
}.openComposeTabDrawer(composeTestRule) {
createCollection(
firstTestPage.title,
secondTestPage.title,
collectionName = collectionName,
)
}.openThreeDotMenu {
}.closeAllTabs {
}
homeScreen {
verifyCollectionIsDisplayed(collectionName)
}.expandCollection(collectionName) {
clickCollectionThreeDotButton(composeTestRule)
selectOpenTabs(composeTestRule)
}
composeTabDrawer(composeTestRule) {
verifyExistingOpenTabs(firstTestPage.title, secondTestPage.title)
}
}
@SmokeTest
@Test
fun shareCollectionTest() {
val firstWebsite = getGenericAsset(mockWebServer, 1)
val secondWebsite = getGenericAsset(mockWebServer, 2)
val sharingApp = "Gmail"
val urlString = "${secondWebsite.url}\n\n${firstWebsite.url}"
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebsite.url) {
}.openComposeTabDrawer(composeTestRule) {
}.openNewTab {
}.submitQuery(secondWebsite.url.toString()) {
waitForPageToLoad()
}.openComposeTabDrawer(composeTestRule) {
createCollection(firstWebsite.title, secondWebsite.title, collectionName = collectionName)
verifySnackBarText("Collection saved!")
}.openThreeDotMenu {
}.closeAllTabs {
verifyCollectionIsDisplayed(collectionName)
}.expandCollection(collectionName) {
}.clickShareCollectionButton {
verifyShareTabsOverlay(firstWebsite.title, secondWebsite.title)
verifySharingWithSelectedApp(sharingApp, urlString, collectionName)
}
}
// Test running on beta/release builds in CI:
// caution when making changes to it, so they don't block the builds
@SmokeTest
@Test
fun deleteCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openComposeTabDrawer(composeTestRule) {
createCollection(webPage.title, collectionName = collectionName)
clickSnackbarButton("VIEW")
}
homeScreen {
verifyCollectionIsDisplayed(collectionName)
}.expandCollection(collectionName) {
clickCollectionThreeDotButton(composeTestRule)
selectDeleteCollection(composeTestRule)
}
homeScreen {
verifySnackBarText("Collection deleted")
verifyNoCollectionsText()
}
}
// open a webpage, and add currently opened tab to existing collection
@Test
fun mainMenuSaveToExistingCollection() {
val firstWebPage = getGenericAsset(mockWebServer, 1)
val secondWebPage = getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
}.openComposeTabDrawer(composeTestRule) {
createCollection(firstWebPage.title, collectionName = collectionName)
verifySnackBarText("Collection saved!")
}.closeTabDrawer {}
navigationToolbar {
}.enterURLAndEnterToBrowser(secondWebPage.url) {
verifyPageContent(secondWebPage.content)
}.openThreeDotMenu {
}.openSaveToCollection {
}.selectExistingCollection(collectionName) {
verifySnackBarText("Tab saved!")
}.goToHomescreen {
verifyCollectionIsDisplayed(collectionName)
}.expandCollection(collectionName) {
verifyTabSavedInCollection(firstWebPage.title)
verifyTabSavedInCollection(secondWebPage.title)
}
}
@Test
fun verifyAddTabButtonOfCollectionMenu() {
val firstWebPage = getGenericAsset(mockWebServer, 1)
val secondWebPage = getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
}.openComposeTabDrawer(composeTestRule) {
createCollection(firstWebPage.title, collectionName = collectionName)
verifySnackBarText("Collection saved!")
closeTab()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(secondWebPage.url) {
}.goToHomescreen {
verifyCollectionIsDisplayed(collectionName)
}.expandCollection(collectionName) {
clickCollectionThreeDotButton(composeTestRule)
selectAddTabToCollection(composeTestRule)
verifyTabsSelectedCounterText(1)
saveTabsSelectedForCollection()
verifySnackBarText("Tab saved!")
verifyTabSavedInCollection(secondWebPage.title)
}
}
@Test
fun renameCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openComposeTabDrawer(composeTestRule) {
createCollection(webPage.title, collectionName = firstCollectionName)
verifySnackBarText("Collection saved!")
}.closeTabDrawer {
}.goToHomescreen {
verifyCollectionIsDisplayed(firstCollectionName)
}.expandCollection(firstCollectionName) {
clickCollectionThreeDotButton(composeTestRule)
selectRenameCollection(composeTestRule)
}.typeCollectionNameAndSave(secondCollectionName) {}
homeScreen {
verifyCollectionIsDisplayed(secondCollectionName)
}
}
@Test
fun createSecondCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openComposeTabDrawer(composeTestRule) {
createCollection(webPage.title, collectionName = firstCollectionName)
verifySnackBarText("Collection saved!")
createCollection(
webPage.title,
collectionName = secondCollectionName,
firstCollection = false,
)
verifySnackBarText("Collection saved!")
}.closeTabDrawer {
}.goToHomescreen {
verifyCollectionIsDisplayed(firstCollectionName)
verifyCollectionIsDisplayed(secondCollectionName)
}
}
@Test
fun removeTabFromCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openComposeTabDrawer(composeTestRule) {
createCollection(webPage.title, collectionName = collectionName)
closeTab()
}
homeScreen {
verifyCollectionIsDisplayed(collectionName)
}.expandCollection(collectionName) {
verifyTabSavedInCollection(webPage.title, true)
removeTabFromCollection(webPage.title)
verifyTabSavedInCollection(webPage.title, false)
}
homeScreen {
verifyCollectionIsDisplayed(collectionName, false)
}
}
@Test
fun undoTabRemovalFromCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openComposeTabDrawer(composeTestRule) {
createCollection(webPage.title, collectionName = collectionName)
closeTab()
}
homeScreen {
verifyCollectionIsDisplayed(collectionName)
}.expandCollection(collectionName) {
verifyTabSavedInCollection(webPage.title, true)
removeTabFromCollection(webPage.title)
}
homeScreen {
verifySnackBarText("Collection deleted")
clickSnackbarButton("UNDO")
verifyCollectionIsDisplayed(collectionName, true)
verifyCollectionIsDisplayed(collectionName, true)
}
}
@Test
fun swipeLeftToRemoveTabFromCollectionTest() {
val testPage = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(testPage.url) {
waitForPageToLoad()
}.openComposeTabDrawer(composeTestRule) {
createCollection(
testPage.title,
collectionName = collectionName,
)
closeTab()
}
homeScreen {
verifyCollectionIsDisplayed(collectionName)
}.expandCollection(collectionName) {
swipeTabLeft(testPage.title, composeTestRule)
verifyTabSavedInCollection(testPage.title, false)
}
homeScreen {
verifySnackBarText("Collection deleted")
verifySnackBarText("UNDO")
verifyCollectionIsDisplayed(collectionName, false)
}
}
@Test
fun swipeRightToRemoveTabFromCollectionTest() {
val testPage = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(testPage.url) {
waitForPageToLoad()
}.openComposeTabDrawer(composeTestRule) {
createCollection(
testPage.title,
collectionName = collectionName,
)
closeTab()
}
homeScreen {
verifyCollectionIsDisplayed(collectionName)
}.expandCollection(collectionName) {
swipeTabRight(testPage.title, composeTestRule)
verifyTabSavedInCollection(testPage.title, false)
}
homeScreen {
verifySnackBarText("Collection deleted")
verifySnackBarText("UNDO")
verifyCollectionIsDisplayed(collectionName, false)
}
}
@Test
fun selectTabOnLongTapTest() {
val firstWebPage = getGenericAsset(mockWebServer, 1)
val secondWebPage = getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
waitForPageToLoad()
}.openComposeTabDrawer(composeTestRule) {
}.openNewTab {
}.submitQuery(secondWebPage.url.toString()) {
waitForPageToLoad()
}.openComposeTabDrawer(composeTestRule) {
verifyExistingOpenTabs(firstWebPage.title, secondWebPage.title)
longClickTab(firstWebPage.title)
verifyTabsMultiSelectionCounter(1)
selectTab(secondWebPage.title)
verifyTabsMultiSelectionCounter(2)
}.clickSaveCollection {
typeCollectionNameAndSave(collectionName)
verifySnackBarText("Tabs saved!")
}
composeTabDrawer(composeTestRule) {
}.closeTabDrawer {
}.goToHomescreen {
}.expandCollection(collectionName) {
verifyTabSavedInCollection(firstWebPage.title)
verifyTabSavedInCollection(secondWebPage.title)
}
}
@Test
fun navigateBackInCollectionFlowTest() {
val webPage = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openComposeTabDrawer(composeTestRule) {
createCollection(webPage.title, collectionName = collectionName)
verifySnackBarText("Collection saved!")
}.closeTabDrawer {
}.openThreeDotMenu {
}.openSaveToCollection {
verifySelectCollectionScreen()
goBackInCollectionFlow()
}
browserScreen {
}.openThreeDotMenu {
}.openSaveToCollection {
verifySelectCollectionScreen()
clickAddNewCollection()
verifyCollectionNameTextField()
goBackInCollectionFlow()
verifySelectCollectionScreen()
goBackInCollectionFlow()
}
// verify the browser layout is visible
browserScreen {
verifyMenuButton()
}
}
@SmokeTest
@Test
fun undoDeleteCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openComposeTabDrawer(composeTestRule) {
createCollection(webPage.title, collectionName = collectionName)
clickSnackbarButton("VIEW")
}
homeScreen {
verifyCollectionIsDisplayed(collectionName)
}.expandCollection(collectionName) {
clickCollectionThreeDotButton(composeTestRule)
selectDeleteCollection(composeTestRule)
}
homeScreen {
verifySnackBarText("Collection deleted")
clickSnackbarButton("UNDO")
verifyCollectionIsDisplayed(collectionName, true)
}
}
}

@ -0,0 +1,302 @@
/* 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.ui
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.core.net.toUri
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
import org.mozilla.fenix.helpers.RetryTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.assertYoutubeAppOpens
import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton
import org.mozilla.fenix.ui.robots.clickContextMenuItem
import org.mozilla.fenix.ui.robots.clickPageObject
import org.mozilla.fenix.ui.robots.downloadRobot
import org.mozilla.fenix.ui.robots.longClickPageObject
import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.shareOverlay
/**
* Tests for verifying basic functionality of content context menus
*
* - Verifies long click "Open link in new tab" UI and functionality
* - Verifies long click "Open link in new Private tab" UI and functionality
* - Verifies long click "Copy Link" UI and functionality
* - Verifies long click "Share Link" UI and functionality
* - Verifies long click "Open image in new tab" UI and functionality
* - Verifies long click "Save Image" UI and functionality
* - Verifies long click "Copy image location" UI and functionality
* - Verifies long click items of mixed hypertext items
*
*/
class ComposeContextMenusTest {
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer
@get:Rule(order = 0)
val composeTestRule =
AndroidComposeTestRule(
HomeActivityIntentTestRule.withDefaultSettingsOverrides(
tabsTrayRewriteEnabled = true,
),
) { it.activity }
@Rule(order = 1)
@JvmField
val retryTestRule = RetryTestRule(3)
@Before
fun setUp() {
composeTestRule.activity.applicationContext.settings().shouldShowJumpBackInCFR = false
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
@SmokeTest
@Test
fun verifyContextOpenLinkNewTab() {
val pageLinks =
TestAssetHelper.getGenericAsset(mockWebServer, 4)
val genericURL =
TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(pageLinks.url) {
mDevice.waitForIdle()
longClickPageObject(itemWithText("Link 1"))
verifyContextMenuForLocalHostLinks(genericURL.url)
clickContextMenuItem("Open link in new tab")
verifySnackBarText("New tab opened")
clickSnackbarButton("SWITCH")
verifyUrl(genericURL.url.toString())
}.openComposeTabDrawer(composeTestRule) {
verifyNormalBrowsingButtonIsSelected()
verifyExistingOpenTabs("Test_Page_1")
verifyExistingOpenTabs("Test_Page_4")
}
}
@SmokeTest
@Test
fun verifyContextOpenLinkPrivateTab() {
val pageLinks =
TestAssetHelper.getGenericAsset(mockWebServer, 4)
val genericURL =
TestAssetHelper.getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(pageLinks.url) {
mDevice.waitForIdle()
longClickPageObject(itemWithText("Link 2"))
verifyContextMenuForLocalHostLinks(genericURL.url)
clickContextMenuItem("Open link in private tab")
verifySnackBarText("New private tab opened")
clickSnackbarButton("SWITCH")
verifyUrl(genericURL.url.toString())
}.openComposeTabDrawer(composeTestRule) {
verifyPrivateBrowsingButtonIsSelected()
verifyExistingOpenTabs("Test_Page_2")
}
}
@Test
fun verifyContextCopyLink() {
val pageLinks =
TestAssetHelper.getGenericAsset(mockWebServer, 4)
val genericURL =
TestAssetHelper.getGenericAsset(mockWebServer, 3)
navigationToolbar {
}.enterURLAndEnterToBrowser(pageLinks.url) {
mDevice.waitForIdle()
longClickPageObject(itemWithText("Link 3"))
verifyContextMenuForLocalHostLinks(genericURL.url)
clickContextMenuItem("Copy link")
verifySnackBarText("Link copied to clipboard")
}.openNavigationToolbar {
}.visitLinkFromClipboard {
verifyUrl(genericURL.url.toString())
}
}
@Test
fun verifyContextCopyLinkNotDisplayedAfterApplied() {
val pageLinks = TestAssetHelper.getGenericAsset(mockWebServer, 4)
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 3)
navigationToolbar {
}.enterURLAndEnterToBrowser(pageLinks.url) {
longClickPageObject(itemWithText("Link 3"))
verifyContextMenuForLocalHostLinks(genericURL.url)
clickContextMenuItem("Copy link")
verifySnackBarText("Link copied to clipboard")
}.openNavigationToolbar {
}.visitLinkFromClipboard {
verifyUrl(genericURL.url.toString())
}.openComposeTabDrawer(composeTestRule) {
}.openNewTab {
}
navigationToolbar {
verifyClipboardSuggestionsAreDisplayed(shouldBeDisplayed = false)
}
}
@Test
fun verifyContextShareLink() {
val pageLinks =
TestAssetHelper.getGenericAsset(mockWebServer, 4)
val genericURL =
TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(pageLinks.url) {
mDevice.waitForIdle()
longClickPageObject(itemWithText("Link 1"))
verifyContextMenuForLocalHostLinks(genericURL.url)
clickContextMenuItem("Share link")
shareOverlay {
verifyShareLinkIntent(genericURL.url)
}
}
}
@Test
fun verifyContextOpenImageNewTab() {
val pageLinks =
TestAssetHelper.getGenericAsset(mockWebServer, 4)
val imageResource =
TestAssetHelper.getImageAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(pageLinks.url) {
mDevice.waitForIdle()
longClickPageObject(itemWithText("test_link_image"))
verifyLinkImageContextMenuItems(imageResource.url)
clickContextMenuItem("Open image in new tab")
verifySnackBarText("New tab opened")
clickSnackbarButton("SWITCH")
verifyUrl(imageResource.url.toString())
}
}
@Test
fun verifyContextCopyImageLocation() {
val pageLinks =
TestAssetHelper.getGenericAsset(mockWebServer, 4)
val imageResource =
TestAssetHelper.getImageAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(pageLinks.url) {
mDevice.waitForIdle()
longClickPageObject(itemWithText("test_link_image"))
verifyLinkImageContextMenuItems(imageResource.url)
clickContextMenuItem("Copy image location")
verifySnackBarText("Link copied to clipboard")
}.openNavigationToolbar {
}.visitLinkFromClipboard {
verifyUrl(imageResource.url.toString())
}
}
@Test
fun verifyContextSaveImage() {
val pageLinks =
TestAssetHelper.getGenericAsset(mockWebServer, 4)
val imageResource =
TestAssetHelper.getImageAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(pageLinks.url) {
mDevice.waitForIdle()
longClickPageObject(itemWithText("test_link_image"))
verifyLinkImageContextMenuItems(imageResource.url)
clickContextMenuItem("Save image")
}
downloadRobot {
verifyDownloadNotificationPopup()
}.clickOpen("image/jpeg") {} // verify open intent is matched with associated data type
downloadRobot {
verifyPhotosAppOpens()
}
}
@Test
fun verifyContextMixedVariations() {
val pageLinks =
TestAssetHelper.getGenericAsset(mockWebServer, 4)
val genericURL =
TestAssetHelper.getGenericAsset(mockWebServer, 1)
val imageResource =
TestAssetHelper.getImageAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(pageLinks.url) {
mDevice.waitForIdle()
longClickPageObject(itemWithText("Link 1"))
verifyContextMenuForLocalHostLinks(genericURL.url)
dismissContentContextMenu()
longClickPageObject(itemWithText("test_link_image"))
verifyLinkImageContextMenuItems(imageResource.url)
dismissContentContextMenu()
longClickPageObject(itemWithText("test_no_link_image"))
verifyNoLinkImageContextMenuItems(imageResource.url)
}
}
@Test
fun verifyContextMixedVariationsInPDFTest() {
val genericURL =
TestAssetHelper.getGenericAsset(mockWebServer, 3)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
clickPageObject(itemWithText("PDF form file"))
waitForPageToLoad()
longClickPageObject(itemWithText("Wikipedia link"))
verifyContextMenuForLinksToOtherHosts("wikipedia.org".toUri())
dismissContentContextMenu()
// Some options are missing from the linked and non liked images context menus in PDF files
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1012805 for more details
longClickPDFImage()
verifyContextMenuForLinksToOtherHosts("wikipedia.org".toUri())
dismissContentContextMenu()
}
}
@Test
fun verifyContextOpenLinkInAppTest() {
val defaultWebPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
longClickPageObject(itemContainingText("Youtube link"))
clickContextMenuItem("Open link in external app")
assertYoutubeAppOpens()
}
}
}

@ -0,0 +1,462 @@
/* 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.ui
import android.content.Context
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.Espresso.pressBack
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import kotlinx.coroutines.runBlocking
import mozilla.components.browser.storage.sync.PlacesHistoryStorage
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.exitMenu
import org.mozilla.fenix.helpers.TestHelper.longTapSelectItem
import org.mozilla.fenix.helpers.TestHelper.registerAndCleanupIdlingResources
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.historyMenu
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.multipleSelectionToolbar
import org.mozilla.fenix.ui.robots.navigationToolbar
/**
* Tests for verifying basic functionality of history
*
*/
class ComposeHistoryTest {
private lateinit var mockWebServer: MockWebServer
private lateinit var mDevice: UiDevice
@get:Rule
val activityTestRule =
AndroidComposeTestRule(
HomeActivityIntentTestRule.withDefaultSettingsOverrides(
tabsTrayRewriteEnabled = true,
),
) { it.activity }
@Before
fun setUp() {
InstrumentationRegistry.getInstrumentation().targetContext.settings()
.shouldShowJumpBackInCFR = false
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@After
fun tearDown() {
mockWebServer.shutdown()
// Clearing all history data after each test to avoid overlapping data
val applicationContext: Context = activityTestRule.activity.applicationContext
val historyStorage = PlacesHistoryStorage(applicationContext)
runBlocking {
historyStorage.deleteEverything()
}
}
@Test
fun noHistoryItemsInCacheTest() {
homeScreen {
}.openThreeDotMenu {
verifyHistoryButton()
}.openHistory {
verifyHistoryMenuView()
verifyEmptyHistoryView()
}
}
// Test running on beta/release builds in CI:
// caution when making changes to it, so they don't block the builds
@Test
fun visitedUrlHistoryTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openHistory {
verifyHistoryListExists()
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1),
) {
verifyHistoryMenuView()
verifyVisitedTimeTitle()
verifyFirstTestPageTitle("Test_Page_1")
verifyTestPageUrl(firstWebPage.url)
}
}
}
@Test
fun deleteHistoryItemTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openHistory {
verifyHistoryListExists()
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1),
) {
clickDeleteHistoryButton(firstWebPage.url.toString())
}
verifyDeleteSnackbarText("Deleted")
verifyEmptyHistoryView()
}
}
@Test
fun undoDeleteHistoryItemTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openHistory {
verifyHistoryListExists()
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1),
) {
clickDeleteHistoryButton(firstWebPage.url.toString())
}
verifyUndoDeleteSnackBarButton()
clickUndoDeleteButton()
verifyHistoryItemExists(true, firstWebPage.url.toString())
}
}
@SmokeTest
@Test
fun cancelDeleteAllHistoryTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openHistory {
verifyHistoryListExists()
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1),
) {
clickDeleteAllHistoryButton()
}
verifyDeleteConfirmationMessage()
selectEverythingOption()
cancelDeleteHistory()
verifyHistoryItemExists(true, firstWebPage.url.toString())
}
}
@SmokeTest
@Test
fun deleteAllHistoryTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openHistory {
verifyHistoryListExists()
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1),
) {
clickDeleteAllHistoryButton()
}
verifyDeleteConfirmationMessage()
selectEverythingOption()
confirmDeleteAllHistory()
verifyDeleteSnackbarText("Browsing data deleted")
verifyEmptyHistoryView()
}
}
@SmokeTest
@Test
fun historyMultiSelectionToolbarItemsTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openHistory {
verifyHistoryListExists()
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1),
) {
longTapSelectItem(firstWebPage.url)
}
}
multipleSelectionToolbar {
verifyMultiSelectionCheckmark()
verifyMultiSelectionCounter()
verifyShareHistoryButton()
verifyCloseToolbarButton()
}.closeToolbarReturnToHistory {
verifyHistoryMenuView()
}
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1807268")
@Test
fun openHistoryInNewTabTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
mDevice.waitForIdle()
}.openComposeTabDrawer(activityTestRule) {
closeTab()
}
homeScreen { }.openThreeDotMenu {
}.openHistory {
verifyHistoryListExists()
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1),
) {
longTapSelectItem(firstWebPage.url)
openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
}
}
multipleSelectionToolbar {
}.clickOpenNewTab(activityTestRule) {
verifyNormalTabsList()
verifyNormalBrowsingButtonIsSelected()
}
}
@Test
fun openHistoryInPrivateTabTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openHistory {
verifyHistoryListExists()
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1),
) {
longTapSelectItem(firstWebPage.url)
openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
}
}
multipleSelectionToolbar {
}.clickOpenPrivateTab(activityTestRule) {
verifyPrivateTabsList()
verifyPrivateBrowsingButtonIsSelected()
}
}
@Test
fun deleteMultipleSelectionTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(secondWebPage.url) {
mDevice.waitForIdle()
verifyUrl(secondWebPage.url.toString())
}.openThreeDotMenu {
}.openHistory {
verifyHistoryListExists()
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 2),
) {
verifyHistoryItemExists(true, firstWebPage.url.toString())
verifyHistoryItemExists(true, secondWebPage.url.toString())
longTapSelectItem(firstWebPage.url)
longTapSelectItem(secondWebPage.url)
openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
}
}
multipleSelectionToolbar {
clickMultiSelectionDelete()
}
historyMenu {
verifyEmptyHistoryView()
}
}
@Test
fun shareButtonTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openHistory {
verifyHistoryListExists()
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1),
) {
longTapSelectItem(firstWebPage.url)
}
}
multipleSelectionToolbar {
clickShareHistoryButton()
verifyShareOverlay()
verifyShareTabFavicon()
verifyShareTabTitle()
verifyShareTabUrl()
}
}
@Test
fun verifySearchHistoryViewTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
}.openHistory {
}.clickSearchButton {
verifySearchView()
verifySearchToolbar(true)
verifySearchSelectorButton()
verifySearchEngineIcon("history")
verifySearchBarPlaceholder("Search history")
verifySearchBarPosition(true)
tapOutsideToDismissSearchBar()
verifySearchToolbar(false)
exitMenu()
}
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openCustomizeSubMenu {
clickTopToolbarToggle()
}
exitMenu()
browserScreen {
}.openThreeDotMenu {
}.openHistory {
}.clickSearchButton {
verifySearchView()
verifySearchToolbar(true)
verifySearchBarPosition(false)
pressBack()
}
historyMenu {
verifyHistoryMenuView()
}
}
@Test
fun verifyVoiceSearchInHistoryTest() {
homeScreen {
}.openThreeDotMenu {
}.openHistory {
}.clickSearchButton {
verifySearchToolbar(true)
verifySearchEngineIcon("history")
startVoiceSearch()
}
}
@Test
fun verifySearchForHistoryItemsTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getHTMLControlsFormAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
}
navigationToolbar {
}.enterURLAndEnterToBrowser(secondWebPage.url) {
}.openThreeDotMenu {
}.openHistory {
}.clickSearchButton {
// Search for a valid term
typeSearch(firstWebPage.title)
verifySearchEngineSuggestionResults(activityTestRule, firstWebPage.url.toString())
verifyNoSuggestionsAreDisplayed(activityTestRule, secondWebPage.url.toString())
clickClearButton()
// Search for invalid term
typeSearch("Android")
verifyNoSuggestionsAreDisplayed(activityTestRule, firstWebPage.url.toString())
verifyNoSuggestionsAreDisplayed(activityTestRule, secondWebPage.url.toString())
}
}
@Test
fun verifyDeletedHistoryItemsCanNotBeSearchedTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
val thirdWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 3)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
verifyPageContent(firstWebPage.content)
}
navigationToolbar {
}.enterURLAndEnterToBrowser(secondWebPage.url) {
verifyPageContent(secondWebPage.content)
}
navigationToolbar {
}.enterURLAndEnterToBrowser(thirdWebPage.url) {
verifyPageContent(thirdWebPage.content)
}.openThreeDotMenu {
}.openHistory {
verifyHistoryListExists()
clickDeleteHistoryButton(firstWebPage.title)
verifyHistoryItemExists(false, firstWebPage.title)
clickDeleteHistoryButton(secondWebPage.title)
verifyHistoryItemExists(false, secondWebPage.title)
}.clickSearchButton {
// Search for a valid term
typeSearch("generic")
verifyNoSuggestionsAreDisplayed(activityTestRule, firstWebPage.url.toString())
verifyNoSuggestionsAreDisplayed(activityTestRule, secondWebPage.url.toString())
verifySearchEngineSuggestionResults(activityTestRule, thirdWebPage.url.toString())
pressBack()
}
historyMenu {
clickDeleteHistoryButton(thirdWebPage.title)
verifyHistoryItemExists(false, firstWebPage.title)
}.clickSearchButton {
// Search for a valid term
typeSearch("generic")
verifyNoSuggestionsAreDisplayed(activityTestRule, thirdWebPage.url.toString())
}
}
}

@ -0,0 +1,272 @@
/* 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.ui
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.Constants.POCKET_RECOMMENDED_STORIES_UTM_PARAM
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.RetryTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
/**
* Tests for verifying the presence of home screen and first-run homescreen elements
*
* Note: For private browsing, navigation bar and tabs see separate test class
*
*/
class ComposeHomeScreenTest {
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer
private lateinit var firstPocketStoryPublisher: String
@get:Rule(order = 0)
val activityTestRule =
AndroidComposeTestRule(
HomeActivityTestRule.withDefaultSettingsOverrides(
tabsTrayRewriteEnabled = true,
),
) { it.activity }
@Rule(order = 1)
@JvmField
val retryTestRule = RetryTestRule(3)
@Before
fun setUp() {
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1844580")
@Test
fun homeScreenItemsTest() {
homeScreen {}.dismissOnboarding()
homeScreen {
verifyHomeWordmark()
verifyHomePrivateBrowsingButton()
verifyExistingTopSitesTabs("Wikipedia")
verifyExistingTopSitesTabs("Top Articles")
verifyExistingTopSitesTabs("Google")
verifyCollectionsHeader()
verifyNoCollectionsText()
scrollToPocketProvokingStories()
verifyThoughtProvokingStories(true)
verifyStoriesByTopicItems()
verifyCustomizeHomepageButton(true)
verifyNavigationToolbar()
verifyHomeMenuButton()
verifyTabButton()
verifyTabCounter("0")
}
}
@Test
fun privateModeScreenItemsTest() {
homeScreen { }.dismissOnboarding()
homeScreen { }.togglePrivateBrowsingMode()
homeScreen {
verifyPrivateBrowsingHomeScreen()
}.openCommonMythsLink {
verifyUrl("common-myths-about-private-browsing")
}
}
@Test
fun verifyJumpBackInSectionTest() {
activityTestRule.activityRule.applySettingsExceptions {
it.isRecentlyVisitedFeatureEnabled = false
it.isPocketEnabled = false
}
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 4)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
verifyPageContent(firstWebPage.content)
verifyUrl(firstWebPage.url.toString())
}.goToHomescreen {
verifyJumpBackInSectionIsDisplayed()
verifyJumpBackInItemTitle(activityTestRule, firstWebPage.title)
verifyJumpBackInItemWithUrl(activityTestRule, firstWebPage.url.toString())
verifyJumpBackInShowAllButton()
}.clickJumpBackInShowAllButton(activityTestRule) {
verifyExistingOpenTabs(firstWebPage.title)
}.closeTabDrawer {
}
navigationToolbar {
}.enterURLAndEnterToBrowser(secondWebPage.url) {
verifyPageContent(secondWebPage.content)
verifyUrl(secondWebPage.url.toString())
}.goToHomescreen {
verifyJumpBackInSectionIsDisplayed()
verifyJumpBackInItemTitle(activityTestRule, secondWebPage.title)
verifyJumpBackInItemWithUrl(activityTestRule, secondWebPage.url.toString())
}.openComposeTabDrawer(activityTestRule) {
closeTabWithTitle(secondWebPage.title)
}.closeTabDrawer {
}
homeScreen {
verifyJumpBackInSectionIsDisplayed()
verifyJumpBackInItemTitle(activityTestRule, firstWebPage.title)
verifyJumpBackInItemWithUrl(activityTestRule, firstWebPage.url.toString())
}.openComposeTabDrawer(activityTestRule) {
closeTab()
}
homeScreen {
verifyJumpBackInSectionIsNotDisplayed()
}
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1844580")
@Test
fun verifyPocketHomepageStoriesTest() {
activityTestRule.activityRule.applySettingsExceptions {
it.isRecentTabsFeatureEnabled = false
it.isRecentlyVisitedFeatureEnabled = false
}
homeScreen {
}.dismissOnboarding()
homeScreen {
verifyThoughtProvokingStories(true)
scrollToPocketProvokingStories()
verifyPocketRecommendedStoriesItems()
// Sponsored Pocket stories are only advertised for a limited time.
// See also known issue https://bugzilla.mozilla.org/show_bug.cgi?id=1828629
// verifyPocketSponsoredStoriesItems(2, 8)
verifyDiscoverMoreStoriesButton()
verifyStoriesByTopic(true)
verifyPoweredByPocket()
}.openThreeDotMenu {
}.openCustomizeHome {
clickPocketButton()
}.goBackToHomeScreen {
verifyThoughtProvokingStories(false)
verifyStoriesByTopic(false)
}
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1844580")
@Test
fun openPocketStoryItemTest() {
activityTestRule.activityRule.applySettingsExceptions {
it.isRecentTabsFeatureEnabled = false
it.isRecentlyVisitedFeatureEnabled = false
}
homeScreen {
}.dismissOnboarding()
homeScreen {
verifyThoughtProvokingStories(true)
scrollToPocketProvokingStories()
firstPocketStoryPublisher = getProvokingStoryPublisher(1)
}.clickPocketStoryItem(firstPocketStoryPublisher, 1) {
verifyUrl(POCKET_RECOMMENDED_STORIES_UTM_PARAM)
}
}
@Test
fun openPocketDiscoverMoreTest() {
activityTestRule.activityRule.applySettingsExceptions {
it.isRecentTabsFeatureEnabled = false
it.isRecentlyVisitedFeatureEnabled = false
}
homeScreen {
}.dismissOnboarding()
homeScreen {
scrollToPocketProvokingStories()
verifyDiscoverMoreStoriesButton()
}.clickPocketDiscoverMoreButton {
verifyUrl("getpocket.com/explore")
}
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1844580")
@Test
fun selectStoriesByTopicItemTest() {
activityTestRule.activityRule.applySettingsExceptions {
it.isRecentTabsFeatureEnabled = false
it.isRecentlyVisitedFeatureEnabled = false
}
homeScreen {
}.dismissOnboarding()
homeScreen {
verifyStoriesByTopicItemState(activityTestRule, false, 1)
clickStoriesByTopicItem(activityTestRule, 1)
verifyStoriesByTopicItemState(activityTestRule, true, 1)
}
}
@Test
fun verifyPocketLearnMoreLinkTest() {
activityTestRule.activityRule.applySettingsExceptions {
it.isRecentTabsFeatureEnabled = false
it.isRecentlyVisitedFeatureEnabled = false
}
homeScreen {
}.dismissOnboarding()
homeScreen {
verifyPoweredByPocket()
}.clickPocketLearnMoreLink(activityTestRule) {
verifyUrl("mozilla.org/en-US/firefox/pocket")
}
}
@Test
fun verifyCustomizeHomepageTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.goToHomescreen {
}.openCustomizeHomepage {
clickJumpBackInButton()
clickRecentBookmarksButton()
clickRecentSearchesButton()
clickPocketButton()
}.goBackToHomeScreen {
verifyCustomizeHomepageButton(false)
}.openThreeDotMenu {
}.openCustomizeHome {
clickJumpBackInButton()
}.goBackToHomeScreen {
verifyCustomizeHomepageButton(true)
}
}
}

@ -0,0 +1,173 @@
package org.mozilla.fenix.ui
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.mediasession.MediaSession
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.MatcherHelper
import org.mozilla.fenix.helpers.RetryTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.clickPageObject
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.notificationShade
/**
* Tests for verifying basic functionality of media notifications:
* - video and audio playback system notifications appear and can pause/play the media content
* - a media notification icon is displayed on the homescreen for the tab playing media content
* Note: this test only verifies media notifications, not media itself
*/
class ComposeMediaNotificationTest {
private lateinit var mockWebServer: MockWebServer
private lateinit var mDevice: UiDevice
private lateinit var browserStore: BrowserStore
@get:Rule(order = 0)
val composeTestRule =
AndroidComposeTestRule(
HomeActivityTestRule.withDefaultSettingsOverrides(
tabsTrayRewriteEnabled = true,
),
) { it.activity }
@Rule(order = 1)
@JvmField
val retryTestRule = RetryTestRule(3)
@Before
fun setUp() {
// Initializing this as part of class construction, below the rule would throw a NPE
// So we are initializing this here instead of in all tests.
browserStore = composeTestRule.activity.components.core.store
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
@SmokeTest
@Test
fun videoPlaybackSystemNotificationTest() {
val videoTestPage = TestAssetHelper.getVideoPageAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(videoTestPage.url) {
mDevice.waitForIdle()
clickPageObject(MatcherHelper.itemWithText("Play"))
assertPlaybackState(browserStore, MediaSession.PlaybackState.PLAYING)
}.openNotificationShade {
verifySystemNotificationExists(videoTestPage.title)
clickMediaNotificationControlButton("Pause")
verifyMediaSystemNotificationButtonState("Play")
}
mDevice.pressBack()
browserScreen {
assertPlaybackState(browserStore, MediaSession.PlaybackState.PAUSED)
}.openComposeTabDrawer(composeTestRule) {
closeTab()
}
mDevice.openNotification()
notificationShade {
verifySystemNotificationDoesNotExist(videoTestPage.title)
}
// close notification shade before the next test
mDevice.pressBack()
}
@SmokeTest
@Test
fun audioPlaybackSystemNotificationTest() {
val audioTestPage = TestAssetHelper.getAudioPageAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(audioTestPage.url) {
mDevice.waitForIdle()
clickPageObject(MatcherHelper.itemWithText("Play"))
assertPlaybackState(browserStore, MediaSession.PlaybackState.PLAYING)
}.openNotificationShade {
verifySystemNotificationExists(audioTestPage.title)
clickMediaNotificationControlButton("Pause")
verifyMediaSystemNotificationButtonState("Play")
}
mDevice.pressBack()
browserScreen {
assertPlaybackState(browserStore, MediaSession.PlaybackState.PAUSED)
}.openComposeTabDrawer(composeTestRule) {
closeTab()
}
mDevice.openNotification()
notificationShade {
verifySystemNotificationDoesNotExist(audioTestPage.title)
}
// close notification shade before the next test
mDevice.pressBack()
}
@Test
fun mediaSystemNotificationInPrivateModeTest() {
val audioTestPage = TestAssetHelper.getAudioPageAsset(mockWebServer)
homeScreen {
}.openComposeTabDrawer(composeTestRule) {
}.toggleToPrivateTabs {
}.openNewTab {
}.submitQuery(audioTestPage.url.toString()) {
mDevice.waitForIdle()
clickPageObject(MatcherHelper.itemWithText("Play"))
assertPlaybackState(browserStore, MediaSession.PlaybackState.PLAYING)
}.openNotificationShade {
verifySystemNotificationExists("A site is playing media")
clickMediaNotificationControlButton("Pause")
verifyMediaSystemNotificationButtonState("Play")
}
mDevice.pressBack()
browserScreen {
assertPlaybackState(browserStore, MediaSession.PlaybackState.PAUSED)
}.openComposeTabDrawer(composeTestRule) {
closeTab()
verifySnackBarText("Private tab closed")
}
mDevice.openNotification()
notificationShade {
verifySystemNotificationDoesNotExist("A site is playing media")
}
// close notification shade before and go back to regular mode before the next test
mDevice.pressBack()
homeScreen { }.togglePrivateBrowsingMode()
}
}

@ -0,0 +1,267 @@
/* 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.ui
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.core.net.toUri
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.runWithSystemLocaleChanged
import org.mozilla.fenix.ui.robots.clickPageObject
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
import java.util.Locale
/**
* Tests for verifying basic functionality of browser navigation and page related interactions
*
* Including:
* - Visiting a URL
* - Back and Forward navigation
* - Refresh
* - Find in page
*/
class ComposeNavigationToolbarTest {
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer
@get:Rule
val composeTestRule =
AndroidComposeTestRule(
HomeActivityTestRule.withDefaultSettingsOverrides(
tabsTrayRewriteEnabled = true,
),
) { it.activity }
@Before
fun setUp() {
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
@Test
fun goBackTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val nextWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
mDevice.waitForIdle()
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(nextWebPage.url) {
verifyUrl(nextWebPage.url.toString())
}.openThreeDotMenu {
}.goToPreviousPage {
mDevice.waitForIdle()
verifyUrl(defaultWebPage.url.toString())
}
}
@Test
fun goForwardTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val nextWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
mDevice.waitForIdle()
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(nextWebPage.url) {
mDevice.waitForIdle()
verifyUrl(nextWebPage.url.toString())
}.openThreeDotMenu {
}.goToPreviousPage {
mDevice.waitForIdle()
verifyUrl(defaultWebPage.url.toString())
}
// Re-open the three-dot menu for verification
navigationToolbar {
}.openThreeDotMenu {
verifyThreeDotMenuExists()
}.goForward {
verifyUrl(nextWebPage.url.toString())
}
}
// Swipes the nav bar left/right to switch between tabs
@SmokeTest
@Test
fun swipeToSwitchTabTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
}.openComposeTabDrawer(composeTestRule) {
}.openNewTab {
}.submitQuery(secondWebPage.url.toString()) {
swipeNavBarRight(secondWebPage.url.toString())
verifyUrl(firstWebPage.url.toString())
swipeNavBarLeft(firstWebPage.url.toString())
verifyUrl(secondWebPage.url.toString())
}
}
// Because it requires changing system prefs, this test will run only on Debug builds
@Test
fun swipeToSwitchTabInRTLTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
val arabicLocale = Locale("ar", "AR")
runWithSystemLocaleChanged(arabicLocale, composeTestRule.activityRule) {
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
}.openComposeTabDrawer(composeTestRule) {
}.openNewTab {
}.submitQuery(secondWebPage.url.toString()) {
swipeNavBarLeft(secondWebPage.url.toString())
verifyUrl(firstWebPage.url.toString())
swipeNavBarRight(firstWebPage.url.toString())
verifyUrl(secondWebPage.url.toString())
}
}
}
// Test running on beta/release builds in CI:
// caution when making changes to it, so they don't block the builds
@Test
fun visitURLTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
verifyUrl(defaultWebPage.url.toString())
}
}
@Test
fun findInPageTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 3)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
mDevice.waitForIdle()
}.openThreeDotMenu {
verifyThreeDotMenuExists()
verifyFindInPageButton()
}.openFindInPage {
verifyFindInPageNextButton()
verifyFindInPagePrevButton()
verifyFindInPageCloseButton()
enterFindInPageQuery("a")
verifyFindNextInPageResult("1/3")
clickFindInPageNextButton()
verifyFindNextInPageResult("2/3")
clickFindInPageNextButton()
verifyFindNextInPageResult("3/3")
clickFindInPagePrevButton()
verifyFindPrevInPageResult("2/3")
clickFindInPagePrevButton()
verifyFindPrevInPageResult("1/3")
}.closeFindInPageWithCloseButton {
verifyFindInPageBar(false)
}.openThreeDotMenu {
}.openFindInPage {
enterFindInPageQuery("3")
verifyFindNextInPageResult("1/1")
}.closeFindInPageWithBackButton {
verifyFindInPageBar(false)
}
}
@Test
fun pdfFindInPageTest() {
val genericURL =
TestAssetHelper.getGenericAsset(mockWebServer, 3)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
clickPageObject(itemWithText("PDF form file"))
}.openThreeDotMenu {
verifyThreeDotMenuExists()
verifyFindInPageButton()
}.openFindInPage {
verifyFindInPageNextButton()
verifyFindInPagePrevButton()
verifyFindInPageCloseButton()
enterFindInPageQuery("l")
verifyFindNextInPageResult("1/2")
clickFindInPageNextButton()
verifyFindNextInPageResult("2/2")
clickFindInPagePrevButton()
verifyFindPrevInPageResult("1/2")
}.closeFindInPageWithCloseButton {
verifyFindInPageBar(false)
}.openThreeDotMenu {
}.openFindInPage {
enterFindInPageQuery("p")
verifyFindNextInPageResult("1/1")
}.closeFindInPageWithBackButton {
verifyFindInPageBar(false)
}
}
@Test
fun verifySecurePageSecuritySubMenuTest() {
val defaultWebPage = "https://mozilla-mobile.github.io/testapp/loginForm"
val defaultWebPageTitle = "Login_form"
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.toUri()) {
}.openSiteSecuritySheet {
verifyQuickActionSheet(defaultWebPage, true)
openSecureConnectionSubMenu(true)
verifySecureConnectionSubMenu(defaultWebPageTitle, defaultWebPage, true)
}
}
@Test
fun verifyInsecurePageSecuritySubMenuTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
waitForPageToLoad()
}.openSiteSecuritySheet {
verifyQuickActionSheet(defaultWebPage.url.toString(), false)
openSecureConnectionSubMenu(false)
verifySecureConnectionSubMenu(defaultWebPage.title, defaultWebPage.url.toString(), false)
}
}
@Test
fun verifyClearCookiesFromQuickSettingsTest() {
val helpPageUrl = "mozilla.org"
homeScreen {
}.openThreeDotMenu {
}.openHelp {
}.openSiteSecuritySheet {
clickQuickActionSheetClearSiteData()
verifyClearSiteDataPrompt(helpPageUrl)
}
}
}

@ -0,0 +1,620 @@
/* 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.ui
import android.content.Context
import android.hardware.camera2.CameraManager
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.core.net.toUri
import androidx.test.espresso.Espresso
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assume
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.Constants
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.MatcherHelper
import org.mozilla.fenix.helpers.MockBrowserDataHelper.setCustomSearchEngine
import org.mozilla.fenix.helpers.SearchDispatcher
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.exitMenu
import org.mozilla.fenix.ui.robots.clickContextMenuItem
import org.mozilla.fenix.ui.robots.clickPageObject
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.longClickPageObject
import org.mozilla.fenix.ui.robots.multipleSelectionToolbar
import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.searchScreen
/**
* Tests for verifying the search fragment
*
* Including:
* - Verify the toolbar, awesomebar, and shortcut bar are displayed
* - Select shortcut button
* - Select scan button
*
*/
class ComposeSearchTest {
lateinit var searchMockServer: MockWebServer
private val queryString: String = "firefox"
private val generalEnginesList = listOf("DuckDuckGo", "Google", "Bing")
private val topicEnginesList = listOf("Amazon.com", "Wikipedia", "eBay")
@get:Rule
val activityTestRule = AndroidComposeTestRule(
HomeActivityTestRule(
skipOnboarding = true,
isPocketEnabled = false,
isJumpBackInCFREnabled = false,
isRecentTabsFeatureEnabled = false,
isTCPCFREnabled = false,
isWallpaperOnboardingEnabled = false,
tabsTrayRewriteEnabled = true,
),
) { it.activity }
@Before
fun setUp() {
searchMockServer = MockWebServer().apply {
dispatcher = SearchDispatcher()
start()
}
}
@After
fun tearDown() {
searchMockServer.shutdown()
}
@Test
fun searchBarItemsTest() {
navigationToolbar {
verifyDefaultSearchEngine("Google")
verifySearchBarPlaceholder("Search or enter address")
}.clickUrlbar {
TestHelper.verifyKeyboardVisibility(isExpectedToBeVisible = true)
verifyScanButtonVisibility(visible = true)
verifyVoiceSearchButtonVisibility(enabled = true)
verifySearchBarPlaceholder("Search or enter address")
typeSearch("mozilla ")
verifyScanButtonVisibility(visible = false)
verifyVoiceSearchButtonVisibility(enabled = true)
}
}
@Test
fun searchSelectorMenuItemsTest() {
homeScreen {
}.openSearch {
verifySearchView()
verifySearchToolbar(isDisplayed = true)
clickSearchSelectorButton()
verifySearchShortcutListContains(
"DuckDuckGo", "Google", "Amazon.com", "Wikipedia", "Bing", "eBay",
"Bookmarks", "Tabs", "History", "Search settings",
)
}
}
@Test
fun searchPlaceholderForDefaultEnginesTest() {
generalEnginesList.forEach {
homeScreen {
}.openSearch {
clickSearchSelectorButton()
}.clickSearchEngineSettings {
openDefaultSearchEngineMenu()
changeDefaultSearchEngine(it)
exitMenu()
}
navigationToolbar {
verifySearchBarPlaceholder("Search or enter address")
}
}
}
@Test
fun searchPlaceholderForOtherGeneralSearchEnginesTest() {
val generalEnginesList = listOf("DuckDuckGo", "Bing")
generalEnginesList.forEach {
homeScreen {
}.openSearch {
clickSearchSelectorButton()
selectTemporarySearchMethod(it)
verifySearchBarPlaceholder("Search the web")
}.dismissSearchBar {}
}
}
@Test
fun searchPlaceholderForTopicSearchEngineTest() {
val topicEnginesList = listOf("Amazon.com", "Wikipedia", "eBay")
topicEnginesList.forEach {
homeScreen {
}.openSearch {
clickSearchSelectorButton()
selectTemporarySearchMethod(it)
verifySearchBarPlaceholder("Enter search terms")
}.dismissSearchBar {}
}
}
@SmokeTest
@Test
fun scanButtonDenyPermissionTest() {
val cameraManager = TestHelper.appContext.getSystemService(Context.CAMERA_SERVICE) as CameraManager
Assume.assumeTrue(cameraManager.cameraIdList.isNotEmpty())
homeScreen {
}.openSearch {
clickScanButton()
TestHelper.denyPermission()
clickScanButton()
clickDismissPermissionRequiredDialog()
}
homeScreen {
}.openSearch {
clickScanButton()
clickGoToPermissionsSettings()
TestHelper.assertNativeAppOpens(Constants.PackageName.ANDROID_SETTINGS)
}
}
@SmokeTest
@Test
fun scanButtonAllowPermissionTest() {
val cameraManager = TestHelper.appContext.getSystemService(Context.CAMERA_SERVICE) as CameraManager
Assume.assumeTrue(cameraManager.cameraIdList.isNotEmpty())
homeScreen {
}.openSearch {
clickScanButton()
TestHelper.grantSystemPermission()
verifyScannerOpen()
}
}
@Test
fun scanButtonAvailableOnlyForGeneralSearchEnginesTest() {
generalEnginesList.forEach {
homeScreen {
}.openSearch {
clickSearchSelectorButton()
selectTemporarySearchMethod(it)
verifyScanButtonVisibility(visible = true)
}.dismissSearchBar {}
}
topicEnginesList.forEach {
homeScreen {
}.openSearch {
clickSearchSelectorButton()
selectTemporarySearchMethod(it)
verifyScanButtonVisibility(visible = false)
}.dismissSearchBar {}
}
}
// Verifies a temporary change of search engine from the Search shortcut menu
@SmokeTest
@Test
fun selectSearchEnginesShortcutTest() {
val enginesList = listOf("DuckDuckGo", "Google", "Amazon.com", "Wikipedia", "Bing", "eBay")
enginesList.forEach {
homeScreen {
}.openSearch {
clickSearchSelectorButton()
verifySearchShortcutListContains(it)
selectTemporarySearchMethod(it)
verifySearchEngineIcon(it)
}.submitQuery("mozilla ") {
verifyUrl("mozilla")
}.goToHomescreen {}
}
}
@Test
fun accessSearchSettingFromSearchSelectorMenuTest() {
searchScreen {
clickSearchSelectorButton()
}.clickSearchEngineSettings {
verifyToolbarText("Search")
openDefaultSearchEngineMenu()
changeDefaultSearchEngine("DuckDuckGo")
TestHelper.exitMenu()
}
homeScreen {
}.openSearch {
}.submitQuery(queryString) {
verifyUrl(queryString)
}
}
@Test
fun clearSearchTest() {
homeScreen {
}.openSearch {
typeSearch(queryString)
clickClearButton()
verifySearchBarPlaceholder("Search or enter address")
}
}
@Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
@SmokeTest
@Test
fun searchGroupShowsInRecentlyVisitedTest() {
val searchEngineName = "TestSearchEngine"
// setting our custom mockWebServer search URL
setCustomSearchEngine(searchMockServer, searchEngineName)
// Performs a search and opens 2 dummy search results links to create a search group
homeScreen {
}.openSearch {
}.submitQuery(queryString) {
longClickPageObject(MatcherHelper.itemWithText("Link 1"))
clickContextMenuItem("Open link in new tab")
TestHelper.clickSnackbarButton("SWITCH")
waitForPageToLoad()
Espresso.pressBack()
longClickPageObject(MatcherHelper.itemWithText("Link 2"))
clickContextMenuItem("Open link in new tab")
TestHelper.clickSnackbarButton("SWITCH")
waitForPageToLoad()
}.openComposeTabDrawer(activityTestRule) {
}.openThreeDotMenu {
}.closeAllTabs {
verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
}
}
@Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
@Test
fun verifySearchGroupHistoryWithNoDuplicatesTest() {
val firstPageUrl = TestAssetHelper.getGenericAsset(searchMockServer, 1).url
val secondPageUrl = TestAssetHelper.getGenericAsset(searchMockServer, 2).url
val originPageUrl =
"http://localhost:${searchMockServer.port}/pages/searchResults.html?search=test%20search".toUri()
val searchEngineName = "TestSearchEngine"
// setting our custom mockWebServer search URL
setCustomSearchEngine(searchMockServer, searchEngineName)
// Performs a search and opens 2 dummy search results links to create a search group
homeScreen {
}.openSearch {
}.submitQuery(queryString) {
longClickPageObject(MatcherHelper.itemWithText("Link 1"))
clickContextMenuItem("Open link in new tab")
TestHelper.clickSnackbarButton("SWITCH")
waitForPageToLoad()
Espresso.pressBack()
longClickPageObject(MatcherHelper.itemWithText("Link 1"))
clickContextMenuItem("Open link in new tab")
TestHelper.clickSnackbarButton("SWITCH")
waitForPageToLoad()
Espresso.pressBack()
longClickPageObject(MatcherHelper.itemWithText("Link 2"))
clickContextMenuItem("Open link in new tab")
TestHelper.clickSnackbarButton("SWITCH")
waitForPageToLoad()
Espresso.pressBack()
longClickPageObject(MatcherHelper.itemWithText("Link 1"))
clickContextMenuItem("Open link in new tab")
TestHelper.clickSnackbarButton("SWITCH")
waitForPageToLoad()
}.openComposeTabDrawer(activityTestRule) {
}.openThreeDotMenu {
}.closeAllTabs {
verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
}.openRecentlyVisitedSearchGroupHistoryList(queryString) {
verifyTestPageUrl(firstPageUrl)
verifyTestPageUrl(secondPageUrl)
verifyTestPageUrl(originPageUrl)
}
}
@Ignore("Failing due to known bug, see https://github.com/mozilla-mobile/fenix/issues/23818")
@Test
fun searchGroupGeneratedInTheSameTabTest() {
// setting our custom mockWebServer search URL
val searchEngineName = "TestSearchEngine"
setCustomSearchEngine(searchMockServer, searchEngineName)
// Performs a search and opens 2 dummy search results links to create a search group
homeScreen {
}.openSearch {
}.submitQuery(queryString) {
clickPageObject(MatcherHelper.itemContainingText("Link 1"))
waitForPageToLoad()
Espresso.pressBack()
clickPageObject(MatcherHelper.itemContainingText("Link 2"))
waitForPageToLoad()
}.openComposeTabDrawer(activityTestRule) {
}.openThreeDotMenu {
}.closeAllTabs {
verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
}
}
@SmokeTest
@Test
fun noSearchGroupFromPrivateBrowsingTest() {
// setting our custom mockWebServer search URL
val searchEngineName = "TestSearchEngine"
setCustomSearchEngine(searchMockServer, searchEngineName)
// Performs a search and opens 2 dummy search results links to create a search group
homeScreen {
}.openSearch {
}.submitQuery(queryString) {
longClickPageObject(MatcherHelper.itemWithText("Link 1"))
clickContextMenuItem("Open link in private tab")
longClickPageObject(MatcherHelper.itemWithText("Link 2"))
clickContextMenuItem("Open link in private tab")
}.openComposeTabDrawer(activityTestRule) {
}.toggleToPrivateTabs {
}.openPrivateTab(0) {
}.openComposeTabDrawer(activityTestRule) {
}.openPrivateTab(1) {
}.openComposeTabDrawer(activityTestRule) {
}.openThreeDotMenu {
}.closeAllTabs {
togglePrivateBrowsingModeOnOff()
verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = false, searchTerm = queryString, groupSize = 3)
}.openThreeDotMenu {
}.openHistory {
verifyHistoryItemExists(shouldExist = false, item = "3 sites")
}
}
@Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
@SmokeTest
@Test
fun deleteItemsFromSearchGroupHistoryTest() {
val firstPageUrl = TestAssetHelper.getGenericAsset(searchMockServer, 1).url
val secondPageUrl = TestAssetHelper.getGenericAsset(searchMockServer, 2).url
// setting our custom mockWebServer search URL
val searchEngineName = "TestSearchEngine"
setCustomSearchEngine(searchMockServer, searchEngineName)
// Performs a search and opens 2 dummy search results links to create a search group
homeScreen {
}.openSearch {
}.submitQuery(queryString) {
longClickPageObject(MatcherHelper.itemWithText("Link 1"))
clickContextMenuItem("Open link in new tab")
TestHelper.clickSnackbarButton("SWITCH")
waitForPageToLoad()
TestHelper.mDevice.pressBack()
longClickPageObject(MatcherHelper.itemWithText("Link 2"))
clickContextMenuItem("Open link in new tab")
TestHelper.clickSnackbarButton("SWITCH")
waitForPageToLoad()
}.openComposeTabDrawer(activityTestRule) {
}.openThreeDotMenu {
}.closeAllTabs {
verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
}.openRecentlyVisitedSearchGroupHistoryList(queryString) {
clickDeleteHistoryButton(firstPageUrl.toString())
TestHelper.longTapSelectItem(secondPageUrl)
multipleSelectionToolbar {
Espresso.openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
clickMultiSelectionDelete()
}
TestHelper.exitMenu()
}
homeScreen {
// checking that the group is removed when only 1 item is left
verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = false, searchTerm = queryString, groupSize = 1)
}
}
@Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
@Test
fun deleteSearchGroupFromHistoryTest() {
val firstPageUrl = TestAssetHelper.getGenericAsset(searchMockServer, 1).url
// setting our custom mockWebServer search URL
val searchEngineName = "TestSearchEngine"
setCustomSearchEngine(searchMockServer, searchEngineName)
// Performs a search and opens 2 dummy search results links to create a search group
homeScreen {
}.openSearch {
}.submitQuery(queryString) {
longClickPageObject(MatcherHelper.itemWithText("Link 1"))
clickContextMenuItem("Open link in new tab")
TestHelper.clickSnackbarButton("SWITCH")
waitForPageToLoad()
TestHelper.mDevice.pressBack()
longClickPageObject(MatcherHelper.itemWithText("Link 2"))
clickContextMenuItem("Open link in new tab")
TestHelper.clickSnackbarButton("SWITCH")
waitForPageToLoad()
}.openComposeTabDrawer(activityTestRule) {
}.openThreeDotMenu {
}.closeAllTabs {
verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
}.openRecentlyVisitedSearchGroupHistoryList(queryString) {
clickDeleteAllHistoryButton()
confirmDeleteAllHistory()
verifyDeleteSnackbarText("Group deleted")
verifyHistoryItemExists(shouldExist = false, firstPageUrl.toString())
}.goBack {}
homeScreen {
verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = false, queryString, groupSize = 3)
}.openThreeDotMenu {
}.openHistory {
verifySearchGroupDisplayed(shouldBeDisplayed = false, queryString, groupSize = 3)
verifyEmptyHistoryView()
}
}
@Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
@Test
fun reopenTabsFromSearchGroupTest() {
val firstPageUrl = TestAssetHelper.getGenericAsset(searchMockServer, 1).url
val secondPageUrl = TestAssetHelper.getGenericAsset(searchMockServer, 2).url
// setting our custom mockWebServer search URL
val searchEngineName = "TestSearchEngine"
setCustomSearchEngine(searchMockServer, searchEngineName)
// Performs a search and opens 2 dummy search results links to create a search group
homeScreen {
}.openSearch {
}.submitQuery(queryString) {
longClickPageObject(MatcherHelper.itemWithText("Link 1"))
clickContextMenuItem("Open link in new tab")
TestHelper.clickSnackbarButton("SWITCH")
waitForPageToLoad()
TestHelper.mDevice.pressBack()
longClickPageObject(MatcherHelper.itemWithText("Link 2"))
clickContextMenuItem("Open link in new tab")
TestHelper.clickSnackbarButton("SWITCH")
waitForPageToLoad()
}.openComposeTabDrawer(activityTestRule) {
}.openThreeDotMenu {
}.closeAllTabs {
verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
}.openRecentlyVisitedSearchGroupHistoryList(queryString) {
}.openWebsite(firstPageUrl) {
verifyUrl(firstPageUrl.toString())
}.goToHomescreen {
}.openRecentlyVisitedSearchGroupHistoryList(queryString) {
TestHelper.longTapSelectItem(firstPageUrl)
TestHelper.longTapSelectItem(secondPageUrl)
Espresso.openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
}
multipleSelectionToolbar {
}.clickOpenNewTab(activityTestRule) {
verifyNormalBrowsingButtonIsSelected()
}.closeTabDrawer {}
Espresso.openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
multipleSelectionToolbar {
}.clickOpenPrivateTab {
verifyPrivateModeSelected()
}
}
@Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
@Test
fun sharePageFromASearchGroupTest() {
val firstPageUrl = TestAssetHelper.getGenericAsset(searchMockServer, 1).url
// setting our custom mockWebServer search URL
val searchEngineName = "TestSearchEngine"
setCustomSearchEngine(searchMockServer, searchEngineName)
// Performs a search and opens 2 dummy search results links to create a search group
homeScreen {
}.openSearch {
}.submitQuery(queryString) {
longClickPageObject(MatcherHelper.itemWithText("Link 1"))
clickContextMenuItem("Open link in new tab")
TestHelper.clickSnackbarButton("SWITCH")
waitForPageToLoad()
TestHelper.mDevice.pressBack()
longClickPageObject(MatcherHelper.itemWithText("Link 2"))
clickContextMenuItem("Open link in new tab")
TestHelper.clickSnackbarButton("SWITCH")
waitForPageToLoad()
}.openComposeTabDrawer(activityTestRule) {
}.openThreeDotMenu {
}.closeAllTabs {
verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
}.openRecentlyVisitedSearchGroupHistoryList(queryString) {
TestHelper.longTapSelectItem(firstPageUrl)
}
multipleSelectionToolbar {
clickShareHistoryButton()
verifyShareOverlay()
verifyShareTabFavicon()
verifyShareTabTitle()
verifyShareTabUrl()
}
}
// Default search code for Google-US
@Test
fun defaultSearchCodeGoogleUS() {
homeScreen {
}.openSearch {
}.submitQuery(queryString) {
waitForPageToLoad()
}.openThreeDotMenu {
}.openHistory {
// Full URL no longer visible in the nav bar, so we'll check the history record
// A search group is sometimes created when searching with Google (probably redirects)
try {
verifyHistoryItemExists(shouldExist = true, Constants.searchEngineCodes["Google"]!!)
} catch (e: AssertionError) {
openSearchGroup(queryString)
verifyHistoryItemExists(shouldExist = true, Constants.searchEngineCodes["Google"]!!)
}
}
}
// Default search code for Bing-US
@Test
fun defaultSearchCodeBingUS() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
openDefaultSearchEngineMenu()
changeDefaultSearchEngine("Bing")
TestHelper.exitMenu()
}
homeScreen {
}.openSearch {
}.submitQuery(queryString) {
waitForPageToLoad()
}.openThreeDotMenu {
}.openHistory {
// Full URL no longer visible in the nav bar, so we'll check the history record
verifyHistoryItemExists(shouldExist = true, Constants.searchEngineCodes["Bing"]!!)
}
}
// Default search code for DuckDuckGo-US
@Test
fun defaultSearchCodeDuckDuckGoUS() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
openDefaultSearchEngineMenu()
changeDefaultSearchEngine("DuckDuckGo")
TestHelper.exitMenu()
}
homeScreen {
}.openSearch {
}.submitQuery(queryString) {
waitForPageToLoad()
}.openThreeDotMenu {
}.openHistory {
// Full URL no longer visible in the nav bar, so we'll check the history record
// A search group is sometimes created when searching with DuckDuckGo
try {
verifyHistoryItemExists(shouldExist = true, item = Constants.searchEngineCodes["DuckDuckGo"]!!)
} catch (e: AssertionError) {
openSearchGroup(queryString)
verifyHistoryItemExists(shouldExist = true, item = Constants.searchEngineCodes["DuckDuckGo"]!!)
}
}
}
}

@ -0,0 +1,260 @@
/* 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.ui
import android.Manifest
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.core.net.toUri
import androidx.test.espresso.Espresso.pressBack
import androidx.test.rule.GrantPermissionRule
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.MatcherHelper
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.getStorageTestAsset
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.deleteDownloadedFileOnStorage
import org.mozilla.fenix.helpers.TestHelper.exitMenu
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.restartApp
import org.mozilla.fenix.helpers.TestHelper.setNetworkEnabled
import org.mozilla.fenix.ui.robots.clickPageObject
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
/**
* Tests for verifying the Settings for:
* Delete Browsing Data on quit
*
*/
class ComposeSettingsDeleteBrowsingDataOnQuitTest {
private lateinit var mockWebServer: MockWebServer
@get:Rule(order = 0)
val composeTestRule =
AndroidComposeTestRule(
HomeActivityIntentTestRule.withDefaultSettingsOverrides(
skipOnboarding = true,
tabsTrayRewriteEnabled = true,
),
) { it.activity }
// Automatically allows app permissions, avoiding a system dialog showing up.
@get:Rule(order = 1)
val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
Manifest.permission.RECORD_AUDIO,
)
@Before
fun setUp() {
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
@Test
fun deleteBrowsingDataOnQuitSettingsItemsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingDataOnQuit {
verifyNavigationToolBarHeader()
verifyDeleteBrowsingOnQuitEnabled(false)
verifyDeleteBrowsingOnQuitButtonSummary()
verifyDeleteBrowsingOnQuitEnabled(false)
clickDeleteBrowsingOnQuitButtonSwitch()
verifyDeleteBrowsingOnQuitEnabled(true)
verifyAllTheCheckBoxesText()
verifyAllTheCheckBoxesChecked(true)
}.goBack {
verifySettingsOptionSummary("Delete browsing data on quit", "On")
}.goBack {
}.openThreeDotMenu {
verifyQuitButtonExists()
pressBack()
}
navigationToolbar {
}.enterURLAndEnterToBrowser("test".toUri()) {
}.openThreeDotMenu {
verifyQuitButtonExists()
}
}
@Test
fun deleteOpenTabsOnQuitTest() {
val testPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingDataOnQuit {
clickDeleteBrowsingOnQuitButtonSwitch()
exitMenu()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(testPage.url) {
}.goToHomescreen {
}.openThreeDotMenu {
clickQuit()
restartApp(composeTestRule.activityRule)
}
homeScreen {
}.openComposeTabDrawer(composeTestRule) {
verifyNoOpenTabsInNormalBrowsing()
}
}
@Test
fun deleteHistoryAndSiteStorageOnQuitTest() {
val storageWritePage =
getStorageTestAsset(mockWebServer, "storage_write.html")
val storageCheckPage =
getStorageTestAsset(mockWebServer, "storage_check.html")
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingDataOnQuit {
clickDeleteBrowsingOnQuitButtonSwitch()
exitMenu()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(storageWritePage.url) {
clickPageObject(MatcherHelper.itemWithText("Set cookies"))
verifyPageContent("Values written to storage")
}.goToHomescreen {
}.openThreeDotMenu {
clickQuit()
restartApp(composeTestRule.activityRule)
}
homeScreen {
}.openThreeDotMenu {
}.openHistory {
verifyEmptyHistoryView()
exitMenu()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(storageCheckPage.url) {
verifyPageContent("Session storage empty")
verifyPageContent("Local storage empty")
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(storageWritePage.url) {
verifyPageContent("No cookies set")
}
}
@SmokeTest
@Test
fun deleteDownloadsOnQuitTest() {
val downloadTestPage = "https://storage.googleapis.com/mobile_test_assets/test_app/downloads.html"
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingDataOnQuit {
clickDeleteBrowsingOnQuitButtonSwitch()
exitMenu()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
}.clickDownloadLink("smallZip.zip") {
verifyDownloadPrompt("smallZip.zip")
}.clickDownload {
verifyDownloadNotificationPopup()
}.closeCompletedDownloadPrompt {
}.goToHomescreen {
}.openThreeDotMenu {
clickQuit()
mDevice.waitForIdle()
}
restartApp(composeTestRule.activityRule)
homeScreen {
}.openThreeDotMenu {
}.openDownloadsManager {
verifyEmptyDownloadsList()
}
deleteDownloadedFileOnStorage("smallZip.zip")
}
@SmokeTest
@Test
fun deleteSitePermissionsOnQuitTest() {
val testPage = "https://mozilla-mobile.github.io/testapp/permissions"
val testPageSubstring = "https://mozilla-mobile.github.io:443"
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingDataOnQuit {
clickDeleteBrowsingOnQuitButtonSwitch()
exitMenu()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(testPage.toUri()) {
waitForPageToLoad()
}.clickStartMicrophoneButton {
verifyMicrophonePermissionPrompt(testPageSubstring)
selectRememberPermissionDecision()
}.clickPagePermissionButton(false) {
verifyPageContent("Microphone not allowed")
}.goToHomescreen {
}.openThreeDotMenu {
clickQuit()
mDevice.waitForIdle()
}
restartApp(composeTestRule.activityRule)
navigationToolbar {
}.enterURLAndEnterToBrowser(testPage.toUri()) {
waitForPageToLoad()
}.clickStartMicrophoneButton {
verifyMicrophonePermissionPrompt(testPageSubstring)
}
}
@Test
fun deleteCachedFilesOnQuitTest() {
val pocketTopArticles = TestHelper.getStringResource(R.string.pocket_pinned_top_articles)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingDataOnQuit {
clickDeleteBrowsingOnQuitButtonSwitch()
exitMenu()
}
homeScreen {
verifyExistingTopSitesTabs(pocketTopArticles)
}.openTopSiteTabWithTitle(pocketTopArticles) {
waitForPageToLoad()
}.goToHomescreen {
}.openThreeDotMenu {
clickQuit()
mDevice.waitForIdle()
}
// disabling wifi to prevent downloads in the background
setNetworkEnabled(enabled = false)
restartApp(composeTestRule.activityRule)
navigationToolbar {
}.enterURLAndEnterToBrowser("about:cache".toUri()) {
verifyNetworkCacheIsEmpty("memory")
verifyNetworkCacheIsEmpty("disk")
}
setNetworkEnabled(enabled = true)
}
}

@ -0,0 +1,269 @@
/* 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.ui
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.getStorageTestAsset
import org.mozilla.fenix.helpers.TestHelper.exitMenu
import org.mozilla.fenix.helpers.TestHelper.getStringResource
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.restartApp
import org.mozilla.fenix.helpers.TestHelper.setNetworkEnabled
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.clickPageObject
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.settingsScreen
/**
* Tests for verifying the Settings for:
* Delete Browsing Data
*/
class ComposeSettingsDeleteBrowsingDataTest {
private lateinit var mockWebServer: MockWebServer
@get:Rule
val composeTestRule =
AndroidComposeTestRule(
HomeActivityIntentTestRule.withDefaultSettingsOverrides(
skipOnboarding = true,
tabsTrayRewriteEnabled = true,
),
) { it.activity }
@Before
fun setUp() {
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
@Test
fun deleteBrowsingDataOptionStatesTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingData {
verifyAllCheckBoxesAreChecked()
switchBrowsingHistoryCheckBox()
switchCachedFilesCheckBox()
verifyOpenTabsCheckBox(true)
verifyBrowsingHistoryDetails(false)
verifyCookiesCheckBox(true)
verifyCachedFilesCheckBox(false)
verifySitePermissionsCheckBox(true)
verifyDownloadsCheckBox(true)
}
restartApp(composeTestRule.activityRule)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingData {
verifyOpenTabsCheckBox(true)
verifyBrowsingHistoryDetails(false)
verifyCookiesCheckBox(true)
verifyCachedFilesCheckBox(false)
verifySitePermissionsCheckBox(true)
verifyDownloadsCheckBox(true)
switchOpenTabsCheckBox()
switchBrowsingHistoryCheckBox()
switchCookiesCheckBox()
switchCachedFilesCheckBox()
switchSitePermissionsCheckBox()
switchDownloadsCheckBox()
verifyOpenTabsCheckBox(false)
verifyBrowsingHistoryDetails(true)
verifyCookiesCheckBox(false)
verifyCachedFilesCheckBox(true)
verifySitePermissionsCheckBox(false)
verifyDownloadsCheckBox(false)
}
restartApp(composeTestRule.activityRule)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingData {
verifyOpenTabsCheckBox(false)
verifyBrowsingHistoryDetails(true)
verifyCookiesCheckBox(false)
verifyCachedFilesCheckBox(true)
verifySitePermissionsCheckBox(false)
verifyDownloadsCheckBox(false)
}
}
@Test
fun deleteTabsDataWithNoOpenTabsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingData {
verifyAllCheckBoxesAreChecked()
selectOnlyOpenTabsCheckBox()
clickDeleteBrowsingDataButton()
verifyDeleteBrowsingDataDialog()
confirmDeletionAndAssertSnackbar()
}
settingsScreen {
verifyGeneralHeading()
}
}
@SmokeTest
@Test
fun deleteTabsDataTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingData {
verifyAllCheckBoxesAreChecked()
selectOnlyOpenTabsCheckBox()
clickDeleteBrowsingDataButton()
verifyDeleteBrowsingDataDialog()
clickDialogCancelButton()
verifyOpenTabsCheckBox(true)
clickDeleteBrowsingDataButton()
verifyDeleteBrowsingDataDialog()
confirmDeletionAndAssertSnackbar()
}
settingsScreen {
verifyGeneralHeading()
}.openSettingsSubMenuDeleteBrowsingData {
verifyOpenTabsDetails("0")
}.goBack {
}.goBack {
}.openComposeTabDrawer(composeTestRule) {
verifyNoOpenTabsInNormalBrowsing()
}
}
@SmokeTest
@Test
fun deleteBrowsingHistoryAndSiteDataTest() {
val storageWritePage = getStorageTestAsset(mockWebServer, "storage_write.html").url
val storageCheckPage = getStorageTestAsset(mockWebServer, "storage_check.html").url
navigationToolbar {
}.enterURLAndEnterToBrowser(storageWritePage) {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(storageCheckPage) {
verifyPageContent("Session storage has value")
verifyPageContent("Local storage has value")
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingData {
verifyBrowsingHistoryDetails("2")
selectOnlyBrowsingHistoryCheckBox()
clickDeleteBrowsingDataButton()
verifyDeleteBrowsingDataDialog()
clickDialogCancelButton()
verifyBrowsingHistoryDetails(true)
clickDeleteBrowsingDataButton()
verifyDeleteBrowsingDataDialog()
confirmDeletionAndAssertSnackbar()
verifyBrowsingHistoryDetails("0")
exitMenu()
}
navigationToolbar {
}.openThreeDotMenu {
}.openHistory {
verifyEmptyHistoryView()
mDevice.pressBack()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(storageCheckPage) {
verifyPageContent("Session storage empty")
verifyPageContent("Local storage empty")
}
}
@SmokeTest
@Test
fun deleteCookiesTest() {
val genericPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val cookiesTestPage = getStorageTestAsset(mockWebServer, "storage_write.html").url
// Browsing a generic page to allow GV to load on a fresh run
navigationToolbar {
}.enterURLAndEnterToBrowser(genericPage.url) {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(cookiesTestPage) {
verifyPageContent("No cookies set")
clickPageObject(itemWithResId("setCookies"))
verifyPageContent("user=android")
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingData {
selectOnlyCookiesCheckBox()
clickDeleteBrowsingDataButton()
verifyDeleteBrowsingDataDialog()
confirmDeletionAndAssertSnackbar()
exitMenu()
}
browserScreen {
}.openThreeDotMenu {
}.refreshPage {
verifyPageContent("No cookies set")
}
}
@SmokeTest
@Test
fun deleteCachedFilesTest() {
val pocketTopArticles = getStringResource(R.string.pocket_pinned_top_articles)
homeScreen {
verifyExistingTopSitesTabs(pocketTopArticles)
}.openTopSiteTabWithTitle(pocketTopArticles) {
waitForPageToLoad()
}.openComposeTabDrawer(composeTestRule) {
}.openNewTab {
}.submitQuery("about:cache") {
// disabling wifi to prevent downloads in the background
setNetworkEnabled(enabled = false)
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingData {
selectOnlyCachedFilesCheckBox()
clickDeleteBrowsingDataButton()
verifyDeleteBrowsingDataDialog()
confirmDeletionAndAssertSnackbar()
exitMenu()
}
browserScreen {
}.openThreeDotMenu {
}.refreshPage {
verifyNetworkCacheIsEmpty("memory")
verifyNetworkCacheIsEmpty("disk")
}
setNetworkEnabled(enabled = true)
}
}

@ -0,0 +1,440 @@
/* 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/. */
@file:Suppress("DEPRECATION")
package org.mozilla.fenix.ui
import android.view.View
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.core.net.toUri
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import androidx.test.uiautomator.UiDevice
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.mediasession.MediaSession
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.IntentReceiverActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
import org.mozilla.fenix.helpers.RetryTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.assertYoutubeAppOpens
import org.mozilla.fenix.helpers.TestHelper.registerAndCleanupIdlingResources
import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.clickPageObject
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
/**
* Test Suite that contains a part of the Smoke and Sanity tests defined in TestRail:
* https://testrail.stage.mozaws.net/index.php?/suites/view/3192
* Other smoke tests have been marked with the @SmokeTest annotation throughout the ui package in order to limit this class expansion.
* These tests will verify different functionalities of the app as a way to quickly detect regressions in main areas
*/
@Suppress("ForbiddenComment")
@SmokeTest
class ComposeSmokeTest {
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer
private val customMenuItem = "TestMenuItem"
private lateinit var browserStore: BrowserStore
@get:Rule(order = 0)
val activityTestRule = AndroidComposeTestRule(
HomeActivityIntentTestRule.withDefaultSettingsOverrides(
tabsTrayRewriteEnabled = true,
),
) { it.activity }
@get: Rule(order = 1)
val intentReceiverActivityTestRule = ActivityTestRule(
IntentReceiverActivity::class.java,
true,
false,
)
@Rule(order = 2)
@JvmField
val retryTestRule = RetryTestRule(3)
@Before
fun setUp() {
// Initializing this as part of class construction, below the rule would throw a NPE
// So we are initializing this here instead of in all related tests.
browserStore = activityTestRule.activity.components.core.store
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
/* Verifies the nav bar:
- opening a web page
- the existence of nav bar items
- editing the url bar
- the tab drawer button
- opening a new search and dismissing the nav bar
*/
@Test
fun verifyBasicNavigationToolbarFunctionality() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
mDevice.waitForIdle()
verifyNavURLBarItems()
}.openNavigationToolbar {
}.goBackToWebsite {
}.openComposeTabDrawer(activityTestRule) {
verifyNormalTabsList()
}.openNewTab {
}.dismissSearchBar {
verifyHomeScreen()
}
}
}
// Device or AVD requires a Google Services Android OS installation with Play Store installed
// Verifies the Open in app button when an app is installed
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1849278")
@Test
fun mainMenuOpenInAppTest() {
val youtubeURL = "https://m.youtube.com/user/mozilla?cbrd=1"
navigationToolbar {
}.enterURLAndEnterToBrowser(youtubeURL.toUri()) {
verifyNotificationDotOnMainMenu()
}.openThreeDotMenu {
}.clickOpenInApp {
assertYoutubeAppOpens()
}
}
// Verifies that deleting a Bookmarks folder also removes the item from inside it.
@Test
fun deleteNonEmptyBookmarkFolderTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(website.url)
}.openThreeDotMenu {
}.openBookmarks {
verifyBookmarkTitle("Test_Page_1")
createFolder("My Folder")
verifyFolderTitle("My Folder")
}.openThreeDotMenu("Test_Page_1") {
}.clickEdit {
clickParentFolderSelector()
selectFolder("My Folder")
navigateUp()
saveEditBookmark()
createFolder("My Folder 2")
verifyFolderTitle("My Folder 2")
}.openThreeDotMenu("My Folder 2") {
}.clickEdit {
clickParentFolderSelector()
selectFolder("My Folder")
navigateUp()
saveEditBookmark()
}.openThreeDotMenu("My Folder") {
}.clickDelete {
cancelFolderDeletion()
verifyFolderTitle("My Folder")
}.openThreeDotMenu("My Folder") {
}.clickDelete {
confirmDeletion()
verifyDeleteSnackBarText()
verifyBookmarkIsDeleted("My Folder")
verifyBookmarkIsDeleted("My Folder 2")
verifyBookmarkIsDeleted("Test_Page_1")
navigateUp()
}
browserScreen {
}.openThreeDotMenu {
verifyAddBookmarkButton()
}
}
@Test
fun shareTabsFromTabsTrayTest() {
val firstWebsite = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebsite = TestAssetHelper.getGenericAsset(mockWebServer, 2)
val firstWebsiteTitle = firstWebsite.title
val secondWebsiteTitle = secondWebsite.title
val sharingApp = "Gmail"
val sharedUrlsString = "${firstWebsite.url}\n\n${secondWebsite.url}"
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(firstWebsite.url) {
verifyPageContent(firstWebsite.content)
}.openComposeTabDrawer(activityTestRule) {
}.openNewTab {
}.submitQuery(secondWebsite.url.toString()) {
verifyPageContent(secondWebsite.content)
}.openComposeTabDrawer(activityTestRule) {
verifyExistingOpenTabs("Test_Page_1")
verifyExistingOpenTabs("Test_Page_2")
}.openThreeDotMenu {
verifyShareAllTabsButton()
}.clickShareAllTabsButton {
verifyShareTabsOverlay(firstWebsiteTitle, secondWebsiteTitle)
verifySharingWithSelectedApp(
sharingApp,
sharedUrlsString,
"$firstWebsiteTitle, $secondWebsiteTitle",
)
}
}
@Test
fun emptyTabsTrayViewPrivateBrowsingTest() {
homeScreen {
}.openComposeTabDrawer(activityTestRule) {
}.toggleToPrivateTabs {
verifyNormalBrowsingButtonIsSelected(false)
verifyPrivateBrowsingButtonIsSelected(true)
verifySyncedTabsButtonIsSelected(false)
verifyNoOpenTabsInPrivateBrowsing()
verifyFab()
verifyThreeDotButton()
}.openThreeDotMenu {
verifyTabSettingsButton()
verifyRecentlyClosedTabsButton()
}
}
@Test
fun privateTabsTrayWithOpenedTabTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.togglePrivateBrowsingMode()
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
}.openComposeTabDrawer(activityTestRule) {
verifyNormalBrowsingButtonIsSelected(false)
verifyPrivateBrowsingButtonIsSelected(true)
verifySyncedTabsButtonIsSelected(false)
verifyThreeDotButton()
verifyNormalTabCounter()
verifyPrivateTabsList()
verifyExistingOpenTabs(website.title)
verifyTabCloseButton()
verifyTabThumbnail()
verifyFab()
}
}
// Test running on beta/release builds in CI:
// caution when making changes to it, so they don't block the builds
@Test
fun noHistoryInPrivateBrowsingTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.togglePrivateBrowsingMode()
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openHistory {
verifyEmptyHistoryView()
}
}
@Test
fun mainMenuInstallPWATest() {
val pwaPage = "https://mozilla-mobile.github.io/testapp/"
navigationToolbar {
}.enterURLAndEnterToBrowser(pwaPage.toUri()) {
verifyNotificationDotOnMainMenu()
}.openThreeDotMenu {
}.clickInstall {
clickAddAutomaticallyButton()
}.openHomeScreenShortcut("TEST_APP") {
mDevice.waitForIdle()
verifyNavURLBarHidden()
}
}
// Verifies that reader mode is detected and the custom appearance controls are displayed
@Test
fun verifyReaderViewAppearanceUI() {
val readerViewPage =
TestAssetHelper.getLoremIpsumAsset(mockWebServer)
val estimatedReadingTime = "1 - 2 minutes"
navigationToolbar {
}.enterURLAndEnterToBrowser(readerViewPage.url) {
mDevice.waitForIdle()
}
registerAndCleanupIdlingResources(
ViewVisibilityIdlingResource(
activityTestRule.activity.findViewById(R.id.mozac_browser_toolbar_page_actions),
View.VISIBLE,
),
) {}
navigationToolbar {
verifyReaderViewDetected(true)
toggleReaderView()
}
browserScreen {
waitForPageToLoad()
verifyPageContent(estimatedReadingTime)
}.openThreeDotMenu {
verifyReaderViewAppearance(true)
}.openReaderViewAppearance {
verifyAppearanceFontGroup(true)
verifyAppearanceFontSansSerif(true)
verifyAppearanceFontSerif(true)
verifyAppearanceFontIncrease(true)
verifyAppearanceFontDecrease(true)
verifyAppearanceColorGroup(true)
verifyAppearanceColorDark(true)
verifyAppearanceColorLight(true)
verifyAppearanceColorSepia(true)
}
}
@Test
fun tabMediaControlButtonTest() {
val audioTestPage = TestAssetHelper.getAudioPageAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(audioTestPage.url) {
mDevice.waitForIdle()
clickPageObject(itemWithText("Play"))
assertPlaybackState(browserStore, MediaSession.PlaybackState.PLAYING)
}.openComposeTabDrawer(activityTestRule) {
verifyTabMediaControlButtonState("Pause")
clickTabMediaControlButton("Pause")
verifyTabMediaControlButtonState("Play")
}.openTab(audioTestPage.title) {
assertPlaybackState(browserStore, MediaSession.PlaybackState.PAUSED)
}
}
// For API>23
// Verifies the default browser switch opens the system default apps menu.
@Test
fun changeDefaultBrowserSetting() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyDefaultBrowserToggle(false)
clickDefaultBrowserSwitch()
verifyAndroidDefaultAppsMenuAppears()
}
// Dismiss the request
mDevice.pressBack()
}
@Test
fun goToHomeScreenBottomToolbarTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
mDevice.waitForIdle()
}.goToHomescreen {
verifyHomeScreen()
}
}
@Test
fun goToHomeScreenTopToolbarTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openCustomizeSubMenu {
clickTopToolbarToggle()
}.goBack {
}.goBack {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
mDevice.waitForIdle()
}.goToHomescreen {
verifyHomeScreen()
}
}
@Test
fun goToHomeScreenBottomToolbarPrivateModeTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
togglePrivateBrowsingModeOnOff()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
mDevice.waitForIdle()
}.goToHomescreen {
verifyHomeScreen()
}
}
@Test
fun goToHomeScreenTopToolbarPrivateModeTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
togglePrivateBrowsingModeOnOff()
}.openThreeDotMenu {
}.openSettings {
}.openCustomizeSubMenu {
clickTopToolbarToggle()
}.goBack {
}.goBack {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
mDevice.waitForIdle()
}.goToHomescreen {
verifyHomeScreen()
}
}
@Test
fun tabsSettingsMenuItemsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openTabsSubMenu {
verifyTabViewOptions()
verifyCloseTabsOptions()
verifyMoveOldTabsToInactiveOptions()
}
}
}

@ -18,6 +18,7 @@ import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.RetryTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton
import org.mozilla.fenix.helpers.TestHelper.verifyKeyboardVisibility
import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.homeScreen
@ -336,7 +337,7 @@ class ComposeTabbedBrowsingTest {
verifyFab()
verifyTabThumbnail()
verifyExistingOpenTabs(defaultWebPage.title)
verifyTabCloseButton(defaultWebPage.title)
verifyTabCloseButton()
}.openTab(defaultWebPage.title) {
verifyUrl(defaultWebPage.url.toString())
verifyTabCounter("1")
@ -355,10 +356,10 @@ class ComposeTabbedBrowsingTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openTabButtonShortcutsMenu {
}.openNewPrivateTabFromShortcutsMenu {
verifyKeyboardVisible()
verifyFocusedNavigationToolbar()
verifyKeyboardVisibility()
verifySearchBarPlaceholder("Search or enter address")
// dismiss search dialog
homeScreen { }.pressBack()
}.dismissSearchBar {
verifyCommonMythsLink()
verifyNavigationToolbar()
}
@ -366,12 +367,25 @@ class ComposeTabbedBrowsingTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openTabButtonShortcutsMenu {
}.openTabFromShortcutsMenu {
verifyKeyboardVisible()
verifyFocusedNavigationToolbar()
verifyKeyboardVisibility()
verifySearchBarPlaceholder("Search or enter address")
// dismiss search dialog
homeScreen { }.pressBack()
}.dismissSearchBar {
verifyHomeWordmark()
verifyNavigationToolbar()
}
}
@Test
fun verifySyncedTabsWhenUserIsNotSignedInTest() {
navigationToolbar {
}.openComposeTabDrawer(composeTestRule) {
verifySyncedTabsButtonIsSelected(isSelected = false)
}.toggleToSyncedTabs {
verifySyncedTabsButtonIsSelected(isSelected = true)
verifySyncedTabsListWhenUserIsNotSignedIn()
}.clickSignInToSyncButton {
verifyTurnOnSyncMenu()
}
}
}

@ -10,6 +10,7 @@ import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
@ -81,7 +82,7 @@ class ContextMenusTest {
}.enterURLAndEnterToBrowser(pageLinks.url) {
mDevice.waitForIdle()
longClickPageObject(itemWithText("Link 1"))
verifyLinkContextMenuItems(genericURL.url)
verifyContextMenuForLocalHostLinks(genericURL.url)
clickContextMenuItem("Open link in new tab")
verifySnackBarText("New tab opened")
clickSnackbarButton("SWITCH")
@ -105,7 +106,7 @@ class ContextMenusTest {
}.enterURLAndEnterToBrowser(pageLinks.url) {
mDevice.waitForIdle()
longClickPageObject(itemWithText("Link 2"))
verifyLinkContextMenuItems(genericURL.url)
verifyContextMenuForLocalHostLinks(genericURL.url)
clickContextMenuItem("Open link in private tab")
verifySnackBarText("New private tab opened")
clickSnackbarButton("SWITCH")
@ -127,7 +128,7 @@ class ContextMenusTest {
}.enterURLAndEnterToBrowser(pageLinks.url) {
mDevice.waitForIdle()
longClickPageObject(itemWithText("Link 3"))
verifyLinkContextMenuItems(genericURL.url)
verifyContextMenuForLocalHostLinks(genericURL.url)
clickContextMenuItem("Copy link")
verifySnackBarText("Link copied to clipboard")
}.openNavigationToolbar {
@ -136,6 +137,7 @@ class ContextMenusTest {
}
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1807268")
@Test
fun verifyContextCopyLinkNotDisplayedAfterApplied() {
val pageLinks = TestAssetHelper.getGenericAsset(mockWebServer, 4)
@ -144,7 +146,7 @@ class ContextMenusTest {
navigationToolbar {
}.enterURLAndEnterToBrowser(pageLinks.url) {
longClickPageObject(itemWithText("Link 3"))
verifyLinkContextMenuItems(genericURL.url)
verifyContextMenuForLocalHostLinks(genericURL.url)
clickContextMenuItem("Copy link")
verifySnackBarText("Link copied to clipboard")
}.openNavigationToolbar {
@ -169,7 +171,7 @@ class ContextMenusTest {
}.enterURLAndEnterToBrowser(pageLinks.url) {
mDevice.waitForIdle()
longClickPageObject(itemWithText("Link 1"))
verifyLinkContextMenuItems(genericURL.url)
verifyContextMenuForLocalHostLinks(genericURL.url)
clickContextMenuItem("Share link")
shareOverlay {
verifyShareLinkIntent(genericURL.url)
@ -252,7 +254,7 @@ class ContextMenusTest {
}.enterURLAndEnterToBrowser(pageLinks.url) {
mDevice.waitForIdle()
longClickPageObject(itemWithText("Link 1"))
verifyLinkContextMenuItems(genericURL.url)
verifyContextMenuForLocalHostLinks(genericURL.url)
dismissContentContextMenu()
longClickPageObject(itemWithText("test_link_image"))
verifyLinkImageContextMenuItems(imageResource.url)
@ -272,12 +274,12 @@ class ContextMenusTest {
clickPageObject(itemWithText("PDF form file"))
waitForPageToLoad()
longClickPageObject(itemWithText("Wikipedia link"))
verifyLinkContextMenuItems("wikipedia.org".toUri(), false)
verifyContextMenuForLinksToOtherHosts("wikipedia.org".toUri())
dismissContentContextMenu()
// Some options are missing from the linked and non liked images context menus in PDF files
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1012805 for more details
longClickPDFImage()
verifyLinkContextMenuItems("wikipedia.org".toUri())
verifyContextMenuForLinksToOtherHosts("wikipedia.org".toUri())
dismissContentContextMenu()
}
}
@ -289,6 +291,7 @@ class ContextMenusTest {
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
longClickPageObject(itemContainingText("Youtube link"))
verifyContextMenuForLinksToOtherApps("youtube.com".toUri())
clickContextMenuItem("Open link in external app")
assertYoutubeAppOpens()
}

@ -18,6 +18,7 @@ import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.FeatureSettingsHelperDelegate
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.createCustomTabIntent
@ -25,6 +26,7 @@ import org.mozilla.fenix.helpers.TestHelper.openAppFromExternalLink
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.clickPageObject
import org.mozilla.fenix.ui.robots.customTabScreen
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.longClickPageObject
import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.notificationShade
@ -35,6 +37,7 @@ class CustomTabsTest {
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer
private val customMenuItem = "TestMenuItem"
private val customTabActionButton = "CustomActionButton"
/* Updated externalLinks.html to v2.0,
changed the hypertext reference to mozilla-mobile.github.io/testapp/downloads for "External link"
@ -204,4 +207,121 @@ class CustomTabsTest {
verifySystemNotificationExists("Download completed")
}
}
// Verifies the main menu of a custom tab with a custom menu item
@SmokeTest
@Test
fun customTabMenuItemsTest() {
val customTabPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
intentReceiverActivityTestRule.launchActivity(
createCustomTabIntent(
customTabPage.url.toString(),
customMenuItem,
),
)
customTabScreen {
verifyCustomTabCloseButton()
}.openMainMenu {
verifyPoweredByTextIsDisplayed()
verifyCustomMenuItem(customMenuItem)
verifyDesktopSiteButtonExists()
verifyFindInPageButtonExists()
verifyOpenInBrowserButtonExists()
verifyBackButtonExists()
verifyForwardButtonExists()
verifyRefreshButtonExists()
}
}
// The test opens a link in a custom tab then sends it to the browser
@Test
fun openCustomTabInBrowserTest() {
val customTabPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
intentReceiverActivityTestRule.launchActivity(
createCustomTabIntent(
customTabPage.url.toString(),
),
)
customTabScreen {
verifyCustomTabCloseButton()
}.openMainMenu {
}.clickOpenInBrowserButton {
verifyTabCounter("1")
}
}
@Test
fun shareCustomTabTest() {
val customTabPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
intentReceiverActivityTestRule.launchActivity(
createCustomTabIntent(
customTabPage.url.toString(),
),
)
customTabScreen {
}.clickShareButton {
verifyShareTabLayout()
}
}
@Test
fun verifyCustomTabViewTest() {
val customTabPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
intentReceiverActivityTestRule.launchActivity(
createCustomTabIntent(
pageUrl = customTabPage.url.toString(),
customActionButtonDescription = customTabActionButton,
),
)
customTabScreen {
verifyCustomTabCloseButton()
verifyCustomTabsSiteInfoButton()
verifyCustomTabToolbarTitle(customTabPage.title)
verifyCustomTabUrl(customTabPage.url.toString())
verifyCustomTabActionButton(customTabActionButton)
verifyCustomTabsShareButton()
verifyMainMenuButton()
clickCustomTabCloseButton()
}
homeScreen {
verifyHomeScreenAppBarItems()
}
}
@Test
fun verifyPdfCustomTabViewTest() {
val customTabPage = TestAssetHelper.getGenericAsset(mockWebServer, 3)
val pdfFormResource = TestAssetHelper.getPdfFormAsset(mockWebServer)
intentReceiverActivityTestRule.launchActivity(
createCustomTabIntent(
customTabPage.url.toString(),
),
)
customTabScreen {
clickPageObject(itemWithText("PDF form file"))
clickPageObject(itemWithResIdAndText("android:id/button2", "CANCEL"))
waitForPageToLoad()
verifyPDFReaderToolbarItems()
verifyCustomTabCloseButton()
verifyCustomTabsSiteInfoButton()
verifyCustomTabToolbarTitle("pdfForm.pdf")
verifyCustomTabUrl(pdfFormResource.url.toString())
verifyCustomTabsShareButton()
verifyMainMenuButton()
clickCustomTabCloseButton()
}
homeScreen {
verifyHomeScreenAppBarItems()
}
}
}

@ -371,13 +371,6 @@ class DownloadTest {
verifySystemNotificationExists("Firefox Fenix")
expandNotificationMessage()
swipeDownloadNotification("Left", false)
verifySystemNotificationExists("Firefox Fenix")
}.closeNotificationTray {
}.openNotificationShade {
verifySystemNotificationExists("Firefox Fenix")
expandNotificationMessage()
swipeDownloadNotification("Right", false)
verifySystemNotificationExists("Firefox Fenix")
clickDownloadNotificationControlButton("CANCEL")
}
deleteDownloadedFileOnStorage(downloadFile)

@ -45,7 +45,7 @@ class HistoryTest {
@get:Rule
val activityTestRule =
AndroidComposeTestRule(
HomeActivityIntentTestRule.withDefaultSettingsOverrides(isUnifiedSearchEnabled = true),
HomeActivityIntentTestRule.withDefaultSettingsOverrides(),
) { it.activity }
@Before

@ -71,7 +71,6 @@ class HomeScreenTest {
verifyStoriesByTopicItems()
verifyCustomizeHomepageButton(true)
verifyNavigationToolbar()
verifyDefaultSearchEngine("Google")
verifyHomeMenuButton()
verifyTabButton()
verifyTabCounter("0")

@ -27,6 +27,7 @@ import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.TestHelper.restartApp
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.clearTextFieldItem
import org.mozilla.fenix.ui.robots.clickPageObject
@ -241,7 +242,6 @@ class LoginsTest {
}
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1816066")
@SmokeTest
@Test
fun verifyMultipleLoginsSelectionsTest() {
@ -344,7 +344,7 @@ class LoginsTest {
}
@Test
fun verifyLoginWithNoUserNameCanBeSavedTest() {
fun verifyLoginWithNoUserNameCanNotBeSavedTest() {
val loginPage = "https://mozilla-mobile.github.io/testapp/loginForm"
val originWebsite = "mozilla-mobile.github.io"
@ -365,12 +365,13 @@ class LoginsTest {
clickThreeDotButton(activityTestRule)
clickEditLoginButton()
clickClearUserNameButton()
saveEditedLogin()
verifyLoginItemUsername("")
verifyUserNameRequiredErrorMessage()
verifySaveLoginButtonIsEnabled(false)
clickGoBackButton()
verifyLoginItemUsername("mozilla")
}
}
@Ignore("https://bugzilla.mozilla.org/show_bug.cgi?id=1840561")
@Test
fun verifyLoginWithoutPasswordCanNotBeSavedTest() {
val loginPage = "https://mozilla-mobile.github.io/testapp/loginForm"
@ -394,7 +395,8 @@ class LoginsTest {
clickEditLoginButton()
clickClearPasswordButton()
verifyPasswordRequiredErrorMessage()
saveEditedLogin()
verifySaveLoginButtonIsEnabled(false)
clickGoBackButton()
revealPassword()
verifyPasswordSaved("firefox")
}
@ -662,7 +664,6 @@ class LoginsTest {
}
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1815650")
@Test
fun verifyLastUsedLoginSortingOptionTest() {
val firstLoginPage = TestAssetHelper.getSaveLoginAsset(mockWebServer)
@ -689,12 +690,12 @@ class LoginsTest {
clickSavedLoginsChevronIcon()
verifyLoginsSortingOptions()
clickLastUsedSortingOption()
verifySortedLogin(activityTestRule, 0, originWebsite)
verifySortedLogin(activityTestRule, 1, firstLoginPage.url.authority.toString())
verifySortedLogin(0, originWebsite)
verifySortedLogin(1, firstLoginPage.url.authority.toString())
}.goBack {
}.openSavedLogins {
verifySortedLogin(activityTestRule, 0, originWebsite)
verifySortedLogin(activityTestRule, 1, firstLoginPage.url.authority.toString())
verifySortedLogin(0, originWebsite)
verifySortedLogin(1, firstLoginPage.url.authority.toString())
}
restartApp(activityTestRule)
@ -704,8 +705,8 @@ class LoginsTest {
}.openSettings {
}.openLoginsAndPasswordSubMenu {
}.openSavedLogins {
verifySortedLogin(activityTestRule, 0, originWebsite)
verifySortedLogin(activityTestRule, 1, firstLoginPage.url.authority.toString())
verifySortedLogin(0, originWebsite)
verifySortedLogin(1, firstLoginPage.url.authority.toString())
}
}
@ -733,12 +734,12 @@ class LoginsTest {
}.openLoginsAndPasswordSubMenu {
}.openSavedLogins {
tapSetupLater()
verifySortedLogin(activityTestRule, 0, firstLoginPage.url.authority.toString())
verifySortedLogin(activityTestRule, 1, originWebsite)
verifySortedLogin(0, firstLoginPage.url.authority.toString())
verifySortedLogin(1, originWebsite)
}.goBack {
}.openSavedLogins {
verifySortedLogin(activityTestRule, 0, firstLoginPage.url.authority.toString())
verifySortedLogin(activityTestRule, 1, originWebsite)
verifySortedLogin(0, firstLoginPage.url.authority.toString())
verifySortedLogin(1, originWebsite)
}
restartApp(activityTestRule)
@ -748,8 +749,8 @@ class LoginsTest {
}.openSettings {
}.openLoginsAndPasswordSubMenu {
}.openSavedLogins {
verifySortedLogin(activityTestRule, 0, firstLoginPage.url.authority.toString())
verifySortedLogin(activityTestRule, 1, originWebsite)
verifySortedLogin(0, firstLoginPage.url.authority.toString())
verifySortedLogin(1, originWebsite)
}
}
@ -790,4 +791,44 @@ class LoginsTest {
verifyPrefilledLoginCredentials("mozilla", "firefox", true)
}
}
@Test
fun verifyCopyUsernameTest() {
val firstLoginPage = TestAssetHelper.getSaveLoginAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstLoginPage.url) {
clickSubmitLoginButton()
verifySaveLoginPromptIsDisplayed()
clickPageObject(itemWithText("Save"))
}.openThreeDotMenu {
}.openSettings {
}.openLoginsAndPasswordSubMenu {
}.openSavedLogins {
tapSetupLater()
viewSavedLoginDetails("test@example.com")
clickCopyUserNameButton()
verifySnackBarText("Username copied to clipboard")
}
}
@Test
fun verifyCopyPasswordTest() {
val firstLoginPage = TestAssetHelper.getSaveLoginAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstLoginPage.url) {
clickSubmitLoginButton()
verifySaveLoginPromptIsDisplayed()
clickPageObject(itemWithText("Save"))
}.openThreeDotMenu {
}.openSettings {
}.openLoginsAndPasswordSubMenu {
}.openSavedLogins {
tapSetupLater()
viewSavedLoginDetails("test@example.com")
clickCopyPasswordButton()
verifySnackBarText("Password copied to clipboard")
}
}
}

@ -14,6 +14,7 @@ import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.verifyDarkThemeApplied
import org.mozilla.fenix.helpers.TestHelper.verifyKeyboardVisibility
import org.mozilla.fenix.helpers.TestHelper.verifyLightThemeApplied
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
@ -131,8 +132,7 @@ class OnboardingTest {
homeScreen {
verifyStartBrowsingButton()
}.openSearch {
verifyScanButton()
verifySearchEngineButton()
verifySearchView()
verifyKeyboardVisibility()
}.dismissSearchBar {
verifyStartBrowsingButton()

@ -35,7 +35,6 @@ class PwaTest {
@get:Rule
val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
@SmokeTest
@Test
fun externalLinkPWATest() {
val externalLinkURL = "https://mozilla-mobile.github.io/testapp/downloads"
@ -57,7 +56,6 @@ class PwaTest {
}
@Ignore("Failing, see https://bugzilla.mozilla.org/show_bug.cgi?id=1807275")
@SmokeTest
@Test
fun emailLinkPWATest() {
navigationToolbar {
@ -74,7 +72,6 @@ class PwaTest {
}
}
@SmokeTest
@Test
fun telephoneLinkPWATest() {
navigationToolbar {
@ -111,7 +108,6 @@ class PwaTest {
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1807273")
@SmokeTest
@Test
fun saveLoginsInPWATest() {
val pwaPage = "https://mozilla-mobile.github.io/testapp/loginForm"

@ -10,9 +10,6 @@ import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.core.net.toUri
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.Espresso.pressBack
import mozilla.components.browser.icons.IconRequest
import mozilla.components.browser.icons.generator.DefaultIconGenerator
import mozilla.components.feature.search.ext.createSearchEngine
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assume.assumeTrue
@ -26,6 +23,7 @@ import org.mozilla.fenix.helpers.Constants.searchEngineCodes
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
import org.mozilla.fenix.helpers.MockBrowserDataHelper.setCustomSearchEngine
import org.mozilla.fenix.helpers.SearchDispatcher
import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
import org.mozilla.fenix.helpers.TestHelper.appContext
@ -36,12 +34,14 @@ import org.mozilla.fenix.helpers.TestHelper.exitMenu
import org.mozilla.fenix.helpers.TestHelper.grantSystemPermission
import org.mozilla.fenix.helpers.TestHelper.longTapSelectItem
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.setCustomSearchEngine
import org.mozilla.fenix.helpers.TestHelper.verifyKeyboardVisibility
import org.mozilla.fenix.ui.robots.clickContextMenuItem
import org.mozilla.fenix.ui.robots.clickPageObject
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.longClickPageObject
import org.mozilla.fenix.ui.robots.multipleSelectionToolbar
import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.searchScreen
/**
* Tests for verifying the search fragment
@ -55,7 +55,9 @@ import org.mozilla.fenix.ui.robots.multipleSelectionToolbar
class SearchTest {
lateinit var searchMockServer: MockWebServer
lateinit var queryString: String
private var queryString = "firefox"
private val generalEnginesList = listOf("DuckDuckGo", "Google", "Bing")
private val topicEnginesList = listOf("Amazon.com", "Wikipedia", "eBay")
@get:Rule
val activityTestRule = AndroidComposeTestRule(
@ -83,13 +85,77 @@ class SearchTest {
}
@Test
fun searchScreenItemsTest() {
fun searchBarItemsTest() {
navigationToolbar {
verifyDefaultSearchEngine("Google")
verifySearchBarPlaceholder("Search or enter address")
}.clickUrlbar {
verifyKeyboardVisibility(isExpectedToBeVisible = true)
verifyScanButtonVisibility(visible = true)
verifyVoiceSearchButtonVisibility(enabled = true)
verifySearchBarPlaceholder("Search or enter address")
typeSearch("mozilla ")
verifyScanButtonVisibility(visible = false)
verifyVoiceSearchButtonVisibility(enabled = true)
}
}
@Test
fun searchSelectorMenuItemsTest() {
homeScreen {
}.openSearch {
verifySearchView()
verifySearchToolbar(true)
verifyScanButton()
verifySearchEngineButton()
verifySearchToolbar(isDisplayed = true)
clickSearchSelectorButton()
verifySearchShortcutListContains(
"DuckDuckGo", "Google", "Amazon.com", "Wikipedia", "Bing", "eBay",
"Bookmarks", "Tabs", "History", "Search settings",
)
}
}
@Test
fun searchPlaceholderForDefaultEnginesTest() {
generalEnginesList.forEach {
homeScreen {
}.openSearch {
clickSearchSelectorButton()
}.clickSearchEngineSettings {
openDefaultSearchEngineMenu()
changeDefaultSearchEngine(it)
exitMenu()
}
navigationToolbar {
verifySearchBarPlaceholder("Search or enter address")
}
}
}
@Test
fun searchPlaceholderForOtherGeneralSearchEnginesTest() {
val generalEnginesList = listOf("DuckDuckGo", "Bing")
generalEnginesList.forEach {
homeScreen {
}.openSearch {
clickSearchSelectorButton()
selectTemporarySearchMethod(it)
verifySearchBarPlaceholder("Search the web")
}.dismissSearchBar {}
}
}
@Test
fun searchPlaceholderForTopicSearchEngineTest() {
val topicEnginesList = listOf("Amazon.com", "Wikipedia", "eBay")
topicEnginesList.forEach {
homeScreen {
}.openSearch {
clickSearchSelectorButton()
selectTemporarySearchMethod(it)
verifySearchBarPlaceholder("Enter search terms")
}.dismissSearchBar {}
}
}
@ -129,40 +195,69 @@ class SearchTest {
}
@Test
fun setDefaultSearchEngineFromShortcutsTest() {
queryString = "firefox"
fun scanButtonAvailableOnlyForGeneralSearchEnginesTest() {
generalEnginesList.forEach {
homeScreen {
}.openSearch {
clickSearchSelectorButton()
selectTemporarySearchMethod(it)
verifyScanButtonVisibility(visible = true)
}.dismissSearchBar {}
}
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
toggleShowSearchShortcuts()
}.goBack {
}.goBack {
}.openSearch {
scrollToSearchEngineSettings(activityTestRule)
}.clickSearchEngineSettings(activityTestRule) {
changeDefaultSearchEngine("DuckDuckGo")
topicEnginesList.forEach {
homeScreen {
}.openSearch {
clickSearchSelectorButton()
selectTemporarySearchMethod(it)
verifyScanButtonVisibility(visible = false)
}.dismissSearchBar {}
}
}
exitMenu()
// Verifies a temporary change of search engine from the Search shortcut menu
@SmokeTest
@Test
fun selectSearchEnginesShortcutTest() {
val enginesList = listOf("DuckDuckGo", "Google", "Amazon.com", "Wikipedia", "Bing", "eBay")
enginesList.forEach {
homeScreen {
}.openSearch {
clickSearchSelectorButton()
verifySearchShortcutListContains(it)
selectTemporarySearchMethod(it)
verifySearchEngineIcon(it)
}.submitQuery("mozilla ") {
verifyUrl("mozilla")
}.goToHomescreen {}
}
}
@Test
fun accessSearchSettingFromSearchSelectorMenuTest() {
searchScreen {
clickSearchSelectorButton()
}.clickSearchEngineSettings {
verifyToolbarText("Search")
openDefaultSearchEngineMenu()
changeDefaultSearchEngine("DuckDuckGo")
exitMenu()
}
homeScreen {
}.openSearch {
}.submitQuery(queryString) {
verifyUrl("duckduckgo.com/?q=firefox")
verifyUrl(queryString)
}
}
@Test
fun clearSearchTest() {
queryString = "test"
homeScreen {
}.openSearch {
typeSearch(queryString)
clickClearButton()
verifySearchBarEmpty()
verifySearchBarPlaceholder("Search or enter address")
}
}
@ -170,16 +265,9 @@ class SearchTest {
@SmokeTest
@Test
fun searchGroupShowsInRecentlyVisitedTest() {
queryString = "test search"
// setting our custom mockWebServer search URL
val searchString =
"http://localhost:${searchMockServer.port}/pages/searchResults.html?search={searchTerms}"
val customSearchEngine = createSearchEngine(
name = "TestSearchEngine",
url = searchString,
icon = DefaultIconGenerator().generate(appContext, IconRequest(searchString)).bitmap,
)
setCustomSearchEngine(customSearchEngine)
val searchEngineName = "TestSearchEngine"
setCustomSearchEngine(searchMockServer, searchEngineName)
// Performs a search and opens 2 dummy search results links to create a search group
homeScreen {
@ -197,7 +285,7 @@ class SearchTest {
}.openTabDrawer {
}.openTabsListThreeDotMenu {
}.closeAllTabs {
verifyRecentlyVisitedSearchGroupDisplayed(true, queryString, 3)
verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
}
}
@ -208,16 +296,9 @@ class SearchTest {
val secondPageUrl = getGenericAsset(searchMockServer, 2).url
val originPageUrl =
"http://localhost:${searchMockServer.port}/pages/searchResults.html?search=test%20search".toUri()
queryString = "test search"
// setting our custom mockWebServer search URL
val searchString =
"http://localhost:${searchMockServer.port}/pages/searchResults.html?search={searchTerms}"
val customSearchEngine = createSearchEngine(
name = "TestSearchEngine",
url = searchString,
icon = DefaultIconGenerator().generate(appContext, IconRequest(searchString)).bitmap,
)
setCustomSearchEngine(customSearchEngine)
val searchEngineName = "TestSearchEngine"
setCustomSearchEngine(searchMockServer, searchEngineName)
// Performs a search and opens 2 dummy search results links to create a search group
homeScreen {
@ -245,7 +326,7 @@ class SearchTest {
}.openTabDrawer {
}.openTabsListThreeDotMenu {
}.closeAllTabs {
verifyRecentlyVisitedSearchGroupDisplayed(true, queryString, 3)
verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
}.openRecentlyVisitedSearchGroupHistoryList(queryString) {
verifyTestPageUrl(firstPageUrl)
verifyTestPageUrl(secondPageUrl)
@ -256,16 +337,9 @@ class SearchTest {
@Ignore("Failing due to known bug, see https://github.com/mozilla-mobile/fenix/issues/23818")
@Test
fun searchGroupGeneratedInTheSameTabTest() {
queryString = "test search"
// setting our custom mockWebServer search URL
val searchString =
"http://localhost:${searchMockServer.port}/pages/searchResults.html?search={searchTerms}"
val customSearchEngine = createSearchEngine(
name = "TestSearchEngine",
url = searchString,
icon = DefaultIconGenerator().generate(appContext, IconRequest(searchString)).bitmap,
)
setCustomSearchEngine(customSearchEngine)
val searchEngineName = "TestSearchEngine"
setCustomSearchEngine(searchMockServer, searchEngineName)
// Performs a search and opens 2 dummy search results links to create a search group
homeScreen {
@ -279,23 +353,16 @@ class SearchTest {
}.openTabDrawer {
}.openTabsListThreeDotMenu {
}.closeAllTabs {
verifyRecentlyVisitedSearchGroupDisplayed(true, queryString, 3)
verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
}
}
@SmokeTest
@Test
fun noSearchGroupFromPrivateBrowsingTest() {
queryString = "test search"
// setting our custom mockWebServer search URL
val searchString =
"http://localhost:${searchMockServer.port}/pages/searchResults.html?search={searchTerms}"
val customSearchEngine = createSearchEngine(
name = "TestSearchEngine",
url = searchString,
icon = DefaultIconGenerator().generate(appContext, IconRequest(searchString)).bitmap,
)
setCustomSearchEngine(customSearchEngine)
val searchEngineName = "TestSearchEngine"
setCustomSearchEngine(searchMockServer, searchEngineName)
// Performs a search and opens 2 dummy search results links to create a search group
homeScreen {
@ -314,10 +381,10 @@ class SearchTest {
}.openTabsListThreeDotMenu {
}.closeAllTabs {
togglePrivateBrowsingModeOnOff()
verifyRecentlyVisitedSearchGroupDisplayed(false, queryString, 3)
verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = false, searchTerm = queryString, groupSize = 3)
}.openThreeDotMenu {
}.openHistory {
verifyHistoryItemExists(false, "3 sites")
verifyHistoryItemExists(shouldExist = false, item = "3 sites")
}
}
@ -325,18 +392,11 @@ class SearchTest {
@SmokeTest
@Test
fun deleteItemsFromSearchGroupHistoryTest() {
queryString = "test search"
val firstPageUrl = getGenericAsset(searchMockServer, 1).url
val secondPageUrl = getGenericAsset(searchMockServer, 2).url
// setting our custom mockWebServer search URL
val searchString =
"http://localhost:${searchMockServer.port}/pages/searchResults.html?search={searchTerms}"
val customSearchEngine = createSearchEngine(
name = "TestSearchEngine",
url = searchString,
icon = DefaultIconGenerator().generate(appContext, IconRequest(searchString)).bitmap,
)
setCustomSearchEngine(customSearchEngine)
val searchEngineName = "TestSearchEngine"
setCustomSearchEngine(searchMockServer, searchEngineName)
// Performs a search and opens 2 dummy search results links to create a search group
homeScreen {
@ -354,7 +414,7 @@ class SearchTest {
}.openTabDrawer {
}.openTabsListThreeDotMenu {
}.closeAllTabs {
verifyRecentlyVisitedSearchGroupDisplayed(true, queryString, 3)
verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
}.openRecentlyVisitedSearchGroupHistoryList(queryString) {
clickDeleteHistoryButton(firstPageUrl.toString())
longTapSelectItem(secondPageUrl)
@ -366,24 +426,17 @@ class SearchTest {
}
homeScreen {
// checking that the group is removed when only 1 item is left
verifyRecentlyVisitedSearchGroupDisplayed(false, queryString, 1)
verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = false, searchTerm = queryString, groupSize = 1)
}
}
@Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
@Test
fun deleteSearchGroupFromHistoryTest() {
queryString = "test search"
val firstPageUrl = getGenericAsset(searchMockServer, 1).url
// setting our custom mockWebServer search URL
val searchString =
"http://localhost:${searchMockServer.port}/pages/searchResults.html?search={searchTerms}"
val customSearchEngine = createSearchEngine(
name = "TestSearchEngine",
url = searchString,
icon = DefaultIconGenerator().generate(appContext, IconRequest(searchString)).bitmap,
)
setCustomSearchEngine(customSearchEngine)
val searchEngineName = "TestSearchEngine"
setCustomSearchEngine(searchMockServer, searchEngineName)
// Performs a search and opens 2 dummy search results links to create a search group
homeScreen {
@ -401,18 +454,18 @@ class SearchTest {
}.openTabDrawer {
}.openTabsListThreeDotMenu {
}.closeAllTabs {
verifyRecentlyVisitedSearchGroupDisplayed(true, queryString, 3)
verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
}.openRecentlyVisitedSearchGroupHistoryList(queryString) {
clickDeleteAllHistoryButton()
confirmDeleteAllHistory()
verifyDeleteSnackbarText("Group deleted")
verifyHistoryItemExists(false, firstPageUrl.toString())
verifyHistoryItemExists(shouldExist = false, firstPageUrl.toString())
}.goBack {}
homeScreen {
verifyRecentlyVisitedSearchGroupDisplayed(false, queryString, 3)
verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = false, queryString, groupSize = 3)
}.openThreeDotMenu {
}.openHistory {
verifySearchGroupDisplayed(false, queryString, 3)
verifySearchGroupDisplayed(shouldBeDisplayed = false, queryString, groupSize = 3)
verifyEmptyHistoryView()
}
}
@ -422,16 +475,9 @@ class SearchTest {
fun reopenTabsFromSearchGroupTest() {
val firstPageUrl = getGenericAsset(searchMockServer, 1).url
val secondPageUrl = getGenericAsset(searchMockServer, 2).url
queryString = "test search"
// setting our custom mockWebServer search URL
val searchString =
"http://localhost:${searchMockServer.port}/pages/searchResults.html?search={searchTerms}"
val customSearchEngine = createSearchEngine(
name = "TestSearchEngine",
url = searchString,
icon = DefaultIconGenerator().generate(appContext, IconRequest(searchString)).bitmap,
)
setCustomSearchEngine(customSearchEngine)
val searchEngineName = "TestSearchEngine"
setCustomSearchEngine(searchMockServer, searchEngineName)
// Performs a search and opens 2 dummy search results links to create a search group
homeScreen {
@ -449,7 +495,7 @@ class SearchTest {
}.openTabDrawer {
}.openTabsListThreeDotMenu {
}.closeAllTabs {
verifyRecentlyVisitedSearchGroupDisplayed(true, queryString, 3)
verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
}.openRecentlyVisitedSearchGroupHistoryList(queryString) {
}.openWebsite(firstPageUrl) {
verifyUrl(firstPageUrl.toString())
@ -475,16 +521,9 @@ class SearchTest {
@Test
fun sharePageFromASearchGroupTest() {
val firstPageUrl = getGenericAsset(searchMockServer, 1).url
queryString = "test search"
// setting our custom mockWebServer search URL
val searchString =
"http://localhost:${searchMockServer.port}/pages/searchResults.html?search={searchTerms}"
val customSearchEngine = createSearchEngine(
name = "TestSearchEngine",
url = searchString,
icon = DefaultIconGenerator().generate(appContext, IconRequest(searchString)).bitmap,
)
setCustomSearchEngine(customSearchEngine)
val searchEngineName = "TestSearchEngine"
setCustomSearchEngine(searchMockServer, searchEngineName)
// Performs a search and opens 2 dummy search results links to create a search group
homeScreen {
@ -502,7 +541,7 @@ class SearchTest {
}.openTabDrawer {
}.openTabsListThreeDotMenu {
}.closeAllTabs {
verifyRecentlyVisitedSearchGroupDisplayed(true, queryString, 3)
verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
}.openRecentlyVisitedSearchGroupHistoryList(queryString) {
longTapSelectItem(firstPageUrl)
}
@ -519,8 +558,6 @@ class SearchTest {
// Default search code for Google-US
@Test
fun defaultSearchCodeGoogleUS() {
queryString = "firefox"
homeScreen {
}.openSearch {
}.submitQuery(queryString) {
@ -541,42 +578,51 @@ class SearchTest {
// Default search code for Bing-US
@Test
fun defaultSearchCodeBingUS() {
queryString = "firefox"
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
openDefaultSearchEngineMenu()
changeDefaultSearchEngine("Bing")
exitMenu()
}
exitMenu()
homeScreen {
}.openSearch {
}.submitQuery(queryString) {
verifyUrl(searchEngineCodes["Bing"]!!)
waitForPageToLoad()
}.openThreeDotMenu {
}.openHistory {
// Full URL no longer visible in the nav bar, so we'll check the history record
verifyHistoryItemExists(shouldExist = true, searchEngineCodes["Bing"]!!)
}
}
// Default search code for DuckDuckGo-US
@Test
fun defaultSearchCodeDuckDuckGoUS() {
queryString = "firefox"
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
openDefaultSearchEngineMenu()
changeDefaultSearchEngine("DuckDuckGo")
exitMenu()
}
exitMenu()
homeScreen {
}.openSearch {
}.submitQuery(queryString) {
verifyUrl(searchEngineCodes["DuckDuckGo"]!!)
waitForPageToLoad()
}.openThreeDotMenu {
}.openHistory {
// Full URL no longer visible in the nav bar, so we'll check the history record
// A search group is sometimes created when searching with DuckDuckGo
try {
verifyHistoryItemExists(shouldExist = true, item = searchEngineCodes["DuckDuckGo"]!!)
} catch (e: AssertionError) {
openSearchGroup(queryString)
verifyHistoryItemExists(shouldExist = true, item = searchEngineCodes["DuckDuckGo"]!!)
}
}
}
}

@ -84,22 +84,6 @@ class SettingsAdvancedTest {
}
}
@SmokeTest
@Test
fun verifyOpenLinkInAppViewInPrivateBrowsingTest() {
homeScreen {
}.togglePrivateBrowsingMode()
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifySettingsOptionSummary("Open links in apps", "Never")
}.openOpenLinksInAppsMenu {
verifyPrivateOpenLinksInAppsView("Never")
}
}
// Assumes Youtube is installed and enabled
@Test
fun neverOpenLinkInAppTest() {
@ -154,7 +138,7 @@ class SettingsAdvancedTest {
// Assumes Youtube is installed and enabled
@SmokeTest
@Test
fun askBeforeOpeningLinkInAppTest() {
fun askBeforeOpeningLinkInAppCancelTest() {
val defaultWebPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
homeScreen {
@ -180,6 +164,28 @@ class SettingsAdvancedTest {
waitForPageToLoad()
verifyUrl("youtube.com")
}
}
// Assumes Youtube is installed and enabled
@SmokeTest
@Test
fun askBeforeOpeningLinkInAppOpenTest() {
val defaultWebPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifySettingsOptionSummary("Open links in apps", "Never")
}.openOpenLinksInAppsMenu {
verifyOpenLinksInAppsView("Never")
clickOpenLinkInAppOption("Ask before opening")
verifySelectedOpenLinksInAppOption("Ask before opening")
}.goBack {
verifySettingsOptionSummary("Open links in apps", "Ask before opening")
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
@ -194,7 +200,7 @@ class SettingsAdvancedTest {
// Assumes Youtube is installed and enabled
@SmokeTest
@Test
fun privateBrowsingAskBeforeOpeningLinkInAppTest() {
fun privateBrowsingAskBeforeOpeningLinkInAppCancelTest() {
val defaultWebPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
homeScreen {
@ -223,6 +229,31 @@ class SettingsAdvancedTest {
waitForPageToLoad()
verifyUrl("youtube.com")
}
}
// Assumes Youtube is installed and enabled
@SmokeTest
@Test
fun privateBrowsingAskBeforeOpeningLinkInAppOpenTest() {
val defaultWebPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
homeScreen {
}.togglePrivateBrowsingMode()
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifySettingsOptionSummary("Open links in apps", "Never")
}.openOpenLinksInAppsMenu {
verifyPrivateOpenLinksInAppsView("Never")
clickOpenLinkInAppOption("Ask before opening")
verifySelectedOpenLinksInAppOption("Ask before opening")
}.goBack {
verifySettingsOptionSummary("Open links in apps", "Ask before opening")
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {

@ -67,6 +67,7 @@ class SettingsHomepageTest {
"Wikipedia",
"Google",
)
val genericURL = getGenericAsset(mockWebServer, 1)
homeScreen {
defaultTopSites.forEach { item ->
@ -80,6 +81,13 @@ class SettingsHomepageTest {
verifyNotExistingTopSitesList(item)
}
}
// Disabling the "Shortcuts" homepage setting option should remove the "Add to shortcuts" from main menu option
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
}.openThreeDotMenu {
expandMenu()
verifyAddToShortcutsButton(shouldExist = false)
}
}
@Test

@ -1,32 +1,41 @@
package org.mozilla.fenix.ui
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.espresso.Espresso.pressBack
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
import org.mozilla.fenix.helpers.MockBrowserDataHelper.addCustomSearchEngine
import org.mozilla.fenix.helpers.SearchDispatcher
import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.appContext
import org.mozilla.fenix.helpers.TestHelper.exitMenu
import org.mozilla.fenix.helpers.TestHelper.runWithCondition
import org.mozilla.fenix.helpers.TestHelper.restartApp
import org.mozilla.fenix.helpers.TestHelper.runWithSystemLocaleChanged
import org.mozilla.fenix.helpers.TestHelper.setSystemLocale
import org.mozilla.fenix.helpers.TestHelper.setTextToClipBoard
import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
import org.mozilla.fenix.ui.robots.EngineShortcut
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.util.ARABIC_LANGUAGE_HEADER
import org.mozilla.fenix.ui.robots.searchScreen
import java.util.Locale
class SettingsSearchTest {
private lateinit var mockWebServer: MockWebServer
private lateinit var searchMockServer: MockWebServer
private val defaultSearchEngineList =
listOf(
"Bing",
"DuckDuckGo",
"Google",
)
@get:Rule
val activityTestRule = AndroidComposeTestRule(
@ -39,6 +48,11 @@ class SettingsSearchTest {
dispatcher = AndroidAssetDispatcher()
start()
}
searchMockServer = MockWebServer().apply {
dispatcher = SearchDispatcher()
start()
}
}
@After
@ -52,28 +66,54 @@ class SettingsSearchTest {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
verifySearchToolbar()
verifyToolbarText("Search")
verifySearchEnginesSectionHeader()
verifyDefaultSearchEngineHeader()
verifySearchEngineList()
verifyShowSearchSuggestions()
verifyShowSearchShortcuts()
verifySearchBrowsingHistory()
verifySearchBookmarks()
verifyShowClipboardSuggestionsDefault()
verifyDefaultSearchEngineSummary("Google")
verifyManageSearchShortcutsHeader()
verifyManageShortcutsSummary()
verifyAddressBarSectionHeader()
verifyAutocompleteURlsIsEnabled(true)
verifyShowClipboardSuggestionsEnabled(true)
verifySearchBrowsingHistoryEnabled(true)
verifySearchBookmarksEnabled(true)
verifySearchSyncedTabsEnabled(true)
verifyVoiceSearchEnabled(true)
verifyShowSearchSuggestionsEnabled(true)
verifyShowSearchSuggestionsInPrivateEnabled(false)
}
}
@Test
fun selectNewDefaultSearchEngine() {
// Goes through the settings and changes the default search engine, then verifies it has changed.
fun defaultSearchEnginesSettingsItemsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
changeDefaultSearchEngine("DuckDuckGo")
}.goBack {
}.goBack {
verifyDefaultSearchEngine("DuckDuckGo")
verifyDefaultSearchEngineHeader()
openDefaultSearchEngineMenu()
verifyToolbarText("Default search engine")
verifyDefaultSearchEngineList()
verifyDefaultSearchEngineSelected("Google")
}
}
@SmokeTest
@Test
fun selectNewDefaultSearchEngine() {
// Goes through the settings and changes the default search engine, then verifies it has changed.
defaultSearchEngineList.forEach {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
openDefaultSearchEngineMenu()
changeDefaultSearchEngine(it)
exitMenu()
}
searchScreen {
verifySearchEngineIcon(it)
}
}
}
@ -100,9 +140,8 @@ class SettingsSearchTest {
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1807268")
@Test
fun toggleSearchBookmarksAndHistoryTest() {
fun toggleSearchHistoryTest() {
val page1 = getGenericAsset(mockWebServer, 1)
val page2 = getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(page1.url) {
@ -125,8 +164,32 @@ class SettingsSearchTest {
closeTab()
}
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
switchSearchHistoryToggle()
exitMenu()
}
homeScreen {
}.openSearch {
typeSearch("test")
verifyNoSuggestionsAreDisplayed(
activityTestRule,
"Firefox Suggest",
"Test_Page_1",
)
}
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1807268")
@Test
fun toggleSearchBookmarksTest() {
val website = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(page2.url) {
}.enterURLAndEnterToBrowser(website.url) {
}.openThreeDotMenu {
}.bookmarkPage {
}.openTabDrawer {
@ -137,11 +200,10 @@ class SettingsSearchTest {
}.openThreeDotMenu {
}.openHistory {
verifyHistoryListExists()
clickDeleteHistoryButton("Test_Page_2")
clickDeleteHistoryButton("Test_Page_1")
exitMenu()
}
exitMenu()
homeScreen {
}.openSearch {
typeSearch("test")
@ -149,10 +211,10 @@ class SettingsSearchTest {
activityTestRule,
"test",
"Firefox Suggest",
"Test_Page_2",
"Test_Page_1",
)
}.clickSearchSuggestion("Test_Page_2") {
verifyUrl(page2.url.toString())
}.clickSearchSuggestion("Test_Page_1") {
verifyUrl(website.url.toString())
}.openTabDrawer {
closeTab()
}
@ -161,12 +223,13 @@ class SettingsSearchTest {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
switchSearchHistoryToggle()
switchSearchBookmarksToggle()
// We want to avoid confusion between history and bookmarks searches,
// so we'll disable this too.
switchSearchHistoryToggle()
exitMenu()
}
exitMenu()
homeScreen {
}.openSearch {
typeSearch("test")
@ -174,78 +237,212 @@ class SettingsSearchTest {
activityTestRule,
"Firefox Suggest",
"Test_Page_1",
"Test_Page_2",
)
}
}
// Ads a new search engine from the list of custom engines
// Verifies setting as default a customized search engine name and URL
@SmokeTest
@Test
fun addPredefinedSearchEngineTest() {
val searchEngine = "Reddit"
fun addCustomDefaultSearchEngineTest() {
val customSearchEngine = object {
val title = "TestSearchEngine"
val url = "http://localhost:${searchMockServer.port}/searchResults.html?search=%s"
}
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
openDefaultSearchEngineMenu()
openAddSearchEngineMenu()
verifyAddSearchEngineList()
addNewSearchEngine(searchEngine)
verifyEngineListContains(searchEngine)
verifySaveSearchEngineButtonEnabled(false)
typeCustomEngineDetails(customSearchEngine.title, customSearchEngine.url)
verifySaveSearchEngineButtonEnabled(true)
saveNewSearchEngine()
verifySnackBarText("Created ${customSearchEngine.title}")
verifyEngineListContains(customSearchEngine.title, shouldExist = true)
openEngineOverflowMenu(customSearchEngine.title)
pressBack()
changeDefaultSearchEngine(customSearchEngine.title)
pressBack()
openManageShortcutsMenu()
verifyEngineListContains(customSearchEngine.title, shouldExist = true)
pressBack()
}.goBack {
verifySettingsOptionSummary("Search", customSearchEngine.title)
}.goBack {
}.openSearch {
verifyKeyboardVisibility()
clickSearchEngineShortcutButton()
verifyEnginesListShortcutContains(activityTestRule, searchEngine)
changeDefaultSearchEngine(activityTestRule, searchEngine)
}.submitQuery("mozilla ") {
verifyUrl(searchEngine)
verifySearchEngineIcon(customSearchEngine.title)
clickSearchSelectorButton()
verifySearchShortcutListContains(customSearchEngine.title)
}
}
@Test
fun addSearchEngineToManageShortcutsListTest() {
val customSearchEngine = object {
val title = "TestSearchEngine"
val url = "http://localhost:${searchMockServer.port}/searchResults.html?search=%s"
}
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifySettingsOptionSummary("Search", "Google")
}.openSearchSubMenu {
changeDefaultSearchEngine(searchEngine)
}.goBack {
verifySettingsOptionSummary("Search", searchEngine)
openManageShortcutsMenu()
openAddSearchEngineMenu()
typeCustomEngineDetails(customSearchEngine.title, customSearchEngine.url)
saveNewSearchEngine()
verifyEngineListContains(customSearchEngine.title, shouldExist = true)
pressBack()
openDefaultSearchEngineMenu()
verifyEngineListContains(customSearchEngine.title, shouldExist = true)
}
}
// Verifies setting as default a customized search engine name and URL
@SmokeTest
@Test
fun editCustomSearchEngineTest() {
searchMockServer = MockWebServer().apply {
dispatcher = SearchDispatcher()
start()
fun addSearchEngineLearnMoreLinksTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
openDefaultSearchEngineMenu()
openAddSearchEngineMenu()
}.clickCustomSearchStringLearnMoreLink {
verifyUrl(
"support.mozilla.org/en-US/kb/manage-my-default-search-engines-firefox-android?as=u&utm_source=inproduct",
)
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
openDefaultSearchEngineMenu()
openAddSearchEngineMenu()
}.clickCustomSearchSuggestionsLearnMoreLink {
verifyUrl(
"support.mozilla.org/en-US/kb/manage-my-default-search-engines-firefox-android?as=u&utm_source=inproduct",
)
}
val searchEngine = object {
}
@Test
fun editCustomSearchEngineTest() {
val customSearchEngine = object {
val title = "TestSearchEngine"
val url = "http://localhost:${searchMockServer.port}/searchResults.html?search=%s"
val newTitle = "Test"
val newTitle = "NewEngineTitle"
}
addCustomSearchEngine(searchMockServer, customSearchEngine.title)
restartApp(activityTestRule.activityRule)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
openAddSearchEngineMenu()
selectAddCustomSearchEngine()
typeCustomEngineDetails(searchEngine.title, searchEngine.url)
saveNewSearchEngine()
openEngineOverflowMenu(searchEngine.title)
openDefaultSearchEngineMenu()
verifyEngineListContains(customSearchEngine.title, shouldExist = true)
openEngineOverflowMenu(customSearchEngine.title)
clickEdit()
typeCustomEngineDetails(searchEngine.newTitle, searchEngine.url)
typeCustomEngineDetails(customSearchEngine.newTitle, customSearchEngine.url)
saveEditSearchEngine()
changeDefaultSearchEngine(searchEngine.newTitle)
}.goBack {
verifySettingsOptionSummary("Search", searchEngine.newTitle)
}.goBack {
}.openSearch {
verifyDefaultSearchEngine(searchEngine.newTitle)
clickSearchEngineShortcutButton()
verifyEnginesListShortcutContains(activityTestRule, searchEngine.newTitle)
verifySnackBarText("Saved ${customSearchEngine.newTitle}")
verifyEngineListContains(customSearchEngine.newTitle, shouldExist = true)
pressBack()
openManageShortcutsMenu()
verifyEngineListContains(customSearchEngine.newTitle, shouldExist = true)
}
}
@Test
fun errorForInvalidSearchEngineStringsTest() {
val customSearchEngine = object {
val title = "TestSearchEngine"
val badTemplateUrl = "http://localhost:${searchMockServer.port}/searchResults.html?search="
val typoUrl = "http://local:${searchMockServer.port}/searchResults.html?search=%s"
val goodUrl = "http://localhost:${searchMockServer.port}/searchResults.html?search=%s"
}
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
openDefaultSearchEngineMenu()
openAddSearchEngineMenu()
typeCustomEngineDetails(customSearchEngine.title, customSearchEngine.badTemplateUrl)
saveNewSearchEngine()
verifyInvalidTemplateSearchStringFormatError()
typeCustomEngineDetails(customSearchEngine.title, customSearchEngine.typoUrl)
saveNewSearchEngine()
verifyErrorConnectingToSearchString(customSearchEngine.title)
typeCustomEngineDetails(customSearchEngine.title, customSearchEngine.goodUrl)
typeSearchEngineSuggestionString(customSearchEngine.badTemplateUrl)
saveNewSearchEngine()
verifyInvalidTemplateSearchStringFormatError()
typeSearchEngineSuggestionString(customSearchEngine.typoUrl)
saveNewSearchEngine()
verifyErrorConnectingToSearchString(customSearchEngine.title)
}
}
@Test
fun deleteCustomSearchEngineTest() {
val customSearchEngineTitle = "TestSearchEngine"
addCustomSearchEngine(mockWebServer, searchEngineName = customSearchEngineTitle)
restartApp(activityTestRule.activityRule)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
openDefaultSearchEngineMenu()
verifyEngineListContains(customSearchEngineTitle, shouldExist = true)
openEngineOverflowMenu(customSearchEngineTitle)
clickDeleteSearchEngine()
verifySnackBarText("Deleted $customSearchEngineTitle")
clickUndoSnackBarButton()
verifyEngineListContains(customSearchEngineTitle, shouldExist = true)
changeDefaultSearchEngine(customSearchEngineTitle)
openEngineOverflowMenu(customSearchEngineTitle)
clickDeleteSearchEngine()
verifyEngineListContains(customSearchEngineTitle, shouldExist = false)
verifyDefaultSearchEngineSelected("Google")
pressBack()
openManageShortcutsMenu()
verifyEngineListContains(customSearchEngineTitle, shouldExist = false)
exitMenu()
}
searchScreen {
clickSearchSelectorButton()
verifySearchShortcutListContains(customSearchEngineTitle, shouldExist = false)
}
}
@Test
fun deleteCustomSearchShortcutTest() {
val customSearchEngineTitle = "TestSearchEngine"
addCustomSearchEngine(mockWebServer, searchEngineName = customSearchEngineTitle)
restartApp(activityTestRule.activityRule)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
openManageShortcutsMenu()
verifyEngineListContains(customSearchEngineTitle, shouldExist = true)
openCustomShortcutOverflowMenu(activityTestRule, customSearchEngineTitle)
clickDeleteSearchEngine(activityTestRule)
verifyEngineListContains(customSearchEngineTitle, shouldExist = false)
pressBack()
openDefaultSearchEngineMenu()
verifyEngineListContains(customSearchEngineTitle, shouldExist = false)
exitMenu()
}
searchScreen {
clickSearchSelectorButton()
verifySearchShortcutListContains(customSearchEngineTitle, shouldExist = false)
}
}
@ -300,7 +497,7 @@ class SettingsSearchTest {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
toggleShowSuggestionsInPrivateSessions()
switchShowSuggestionsInPrivateSessionsToggle()
}.goBack {
}.goBack {
}.openSearch {
@ -309,7 +506,6 @@ class SettingsSearchTest {
}
}
@SmokeTest
@Test
fun toggleVoiceSearchTest() {
homeScreen {
@ -341,8 +537,9 @@ class SettingsSearchTest {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
verifyShowClipboardSuggestionsDefault()
verifyShowClipboardSuggestionsEnabled(true)
toggleClipboardSuggestion()
verifyShowClipboardSuggestionsEnabled(false)
exitMenu()
}
homeScreen {
@ -351,194 +548,103 @@ class SettingsSearchTest {
}
}
// Expected for en-us defaults
@Test
fun undoDeleteSearchEngineTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
verifyEngineListContains("Bing")
openEngineOverflowMenu("Bing")
clickDeleteSearchEngine()
clickUndoSnackBarButton()
verifyEngineListContains("Bing")
}
}
// Expected for en-us defaults
@Test
fun deleteDefaultSearchEngineTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
verifyEngineListContains("Google")
verifyDefaultSearchEngine("Google")
openEngineOverflowMenu("Google")
clickDeleteSearchEngine()
verifyEngineListDoesNotContain("Google")
verifyDefaultSearchEngine("Bing")
}
}
// Expected for en-us defaults
// Expected for app language set to Arabic
@Test
fun deleteAllSearchEnginesTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
runWithCondition(!appContext.settings().showUnifiedSearchFeature) {
// If the feature is disabled run old steps.
deleteMultipleSearchEngines(
"Google",
"Bing",
"Amazon.com",
"DuckDuckGo",
"eBay",
)
verifyDefaultSearchEngine("Wikipedia")
verifyThreeDotButtonIsNotDisplayed("Wikipedia")
openAddSearchEngineMenu()
verifyAddSearchEngineListContains(
fun verifySearchEnginesWithRTLLocale() {
val arabicLocale = Locale("ar", "AR")
runWithSystemLocaleChanged(arabicLocale, activityTestRule.activityRule) {
homeScreen {
}.openSearch {
verifyTranslatedFocusedNavigationToolbar("ابحث أو أدخِل عنوانا")
clickSearchSelectorButton()
verifySearchShortcutListContains(
"Google",
"Bing",
"Amazon.com",
"DuckDuckGo",
"eBay",
"ويكيبيديا (ar)",
)
selectTemporarySearchMethod("ويكيبيديا (ar)")
}.submitQuery("firefox") {
verifyUrl("firefox")
}
runWithCondition(appContext.settings().showUnifiedSearchFeature) {
// Run steps suitable for the enabled unified search feature.
deleteMultipleSearchEngines(
}
}
@Test
fun searchEnginesListRespectLocaleTest() {
runWithSystemLocaleChanged(Locale.CHINA, activityTestRule.activityRule) {
// Checking search engines for CH locale
homeScreen {
}.openSearch {
clickSearchSelectorButton()
verifySearchShortcutListContains(
"Google",
"百度",
"Bing",
"Amazon.com",
"eBay",
"Wikipedia",
"DuckDuckGo",
)
verifyDefaultSearchEngine("DuckDuckGo")
verifyThreeDotButtonIsNotDisplayed("DuckDuckGo")
openAddSearchEngineMenu()
verifyAddSearchEngineListContains(
}.dismissSearchBar {}
// Checking search engines for FR locale
setSystemLocale(Locale.FRENCH)
homeScreen {
}.openSearch {
clickSearchSelectorButton()
verifySearchShortcutListContains(
"Google",
"Bing",
"Amazon.com",
"eBay",
"Wikipedia",
"DuckDuckGo",
"Qwant",
"Wikipédia (fr)",
)
}
}
}
// Expected for en-us defaults
@Test
fun changeSearchEnginesBasedOnTextTest() {
homeScreen {
}.openSearch {
typeSearch("D")
verifySearchEnginePrompt(activityTestRule, "DuckDuckGo")
clickSearchEnginePrompt(activityTestRule, "DuckDuckGo")
}.submitQuery("firefox") {
verifyUrl("duckduckgo.com/?q=firefox")
}
}
// Expected for app language set to Arabic
@Test
fun verifySearchEnginesWithRTLLocale() {
fun manageSearchShortcutsSettingsItemsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
toggleShowSearchShortcuts()
}.goBack {
}.openLanguageSubMenu {
TestHelper.registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(
activityTestRule.activity.findViewById(R.id.locale_list),
2,
),
) {
selectLanguage("Arabic")
verifyLanguageHeaderIsTranslated(ARABIC_LANGUAGE_HEADER)
}
}
exitMenu()
homeScreen {
}.openSearch {
verifyTranslatedFocusedNavigationToolbar("ابحث أو أدخِل عنوانا")
verifySearchEngineShortcuts(
activityTestRule,
"Google",
"Bing",
"Amazon.com",
"DuckDuckGo",
"ويكيبيديا (ar)",
openManageShortcutsMenu()
verifyToolbarText("Manage search shortcuts")
verifyEnginesShortcutsListHeader()
verifyManageShortcutsList(activityTestRule)
verifySearchShortcutChecked(
EngineShortcut(name = "Google", checkboxIndex = 1, isChecked = true),
EngineShortcut(name = "Bing", checkboxIndex = 4, isChecked = true),
EngineShortcut(name = "Amazon.com", checkboxIndex = 7, isChecked = true),
EngineShortcut(name = "DuckDuckGo", checkboxIndex = 10, isChecked = true),
EngineShortcut(name = "eBay", checkboxIndex = 13, isChecked = true),
EngineShortcut(name = "Wikipedia", checkboxIndex = 16, isChecked = true),
EngineShortcut(name = "Reddit", checkboxIndex = 19, isChecked = false),
EngineShortcut(name = "YouTube", checkboxIndex = 22, isChecked = false),
)
changeDefaultSearchEngine(activityTestRule, "ويكيبيديا (ar)")
}.submitQuery("firefox") {
verifyUrl("ar.m.wikipedia.org")
}
}
// Expected for en-us defaults
@SmokeTest
@Test
fun toggleSearchEnginesShortcutListTest() {
fun changeSearchShortcutsListTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
verifyShowSearchEnginesToggleState(false)
toggleShowSearchShortcuts()
verifyShowSearchEnginesToggleState(true)
}
exitMenu()
homeScreen {
}.openSearch {
verifySearchEngineShortcuts(
activityTestRule,
"Google",
"Bing",
"Amazon.com",
"DuckDuckGo",
"eBay",
"Wikipedia",
)
scrollToSearchEngineSettings(activityTestRule)
}.clickSearchEngineSettings(activityTestRule) {
toggleShowSearchShortcuts()
verifyShowSearchEnginesToggleState(false)
openManageShortcutsMenu()
selectSearchShortcut(EngineShortcut(name = "Google", checkboxIndex = 1))
selectSearchShortcut(EngineShortcut(name = "Amazon.com", checkboxIndex = 7))
selectSearchShortcut(EngineShortcut(name = "Reddit", checkboxIndex = 19))
selectSearchShortcut(EngineShortcut(name = "YouTube", checkboxIndex = 22))
exitMenu()
}
exitMenu()
homeScreen {
}.openSearch {
verifySearchEngineShortcutsAreNotDisplayed(
activityTestRule,
"Google",
"Bing",
"Amazon.com",
"DuckDuckGo",
"eBay",
"Wikipedia",
)
clickSearchEngineShortcutButton()
verifySearchEngineShortcuts(
activityTestRule,
"Google",
"Bing",
"Amazon.com",
"DuckDuckGo",
"eBay",
"Wikipedia",
)
searchScreen {
clickSearchSelectorButton()
verifySearchShortcutListContains("Google", "Amazon.com", shouldExist = false)
verifySearchShortcutListContains("YouTube", shouldExist = true)
verifySearchShortcutListContains("Reddit", shouldExist = true)
}
}
}

@ -17,6 +17,7 @@ import mozilla.components.concept.engine.mediasession.MediaSession
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.IntentReceiverActivity
@ -29,12 +30,10 @@ import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
import org.mozilla.fenix.helpers.RetryTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.assertYoutubeAppOpens
import org.mozilla.fenix.helpers.TestHelper.createCustomTabIntent
import org.mozilla.fenix.helpers.TestHelper.registerAndCleanupIdlingResources
import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.clickPageObject
import org.mozilla.fenix.ui.robots.customTabScreen
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
@ -116,6 +115,7 @@ class SmokeTest {
// Device or AVD requires a Google Services Android OS installation with Play Store installed
// Verifies the Open in app button when an app is installed
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1849278")
@Test
fun mainMenuOpenInAppTest() {
val youtubeURL = "https://m.youtube.com/user/mozilla?cbrd=1"
@ -129,25 +129,6 @@ class SmokeTest {
}
}
// Verifies changing the default engine from the Search Shortcut menu
@Test
fun selectSearchEnginesShortcutTest() {
val enginesList = listOf("DuckDuckGo", "Google", "Amazon.com", "Wikipedia", "Bing", "eBay")
for (searchEngine in enginesList) {
homeScreen {
}.openSearch {
verifyKeyboardVisibility()
clickSearchEngineShortcutButton()
verifySearchEngineList(activityTestRule)
changeDefaultSearchEngine(activityTestRule, searchEngine)
verifySearchEngineIcon(searchEngine)
}.submitQuery("mozilla ") {
verifyUrl(searchEngine)
}.goToHomescreen { }
}
}
// Verifies that deleting a Bookmarks folder also removes the item from inside it.
@Test
fun deleteNonEmptyBookmarkFolderTest() {
@ -342,51 +323,6 @@ class SmokeTest {
}
}
// Verifies the main menu of a custom tab with a custom menu item
@Test
fun customTabMenuItemsTest() {
val customTabPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
intentReceiverActivityTestRule.launchActivity(
createCustomTabIntent(
customTabPage.url.toString(),
customMenuItem,
),
)
customTabScreen {
verifyCustomTabCloseButton()
}.openMainMenu {
verifyPoweredByTextIsDisplayed()
verifyCustomMenuItem(customMenuItem)
verifyDesktopSiteButtonExists()
verifyFindInPageButtonExists()
verifyOpenInBrowserButtonExists()
verifyBackButtonExists()
verifyForwardButtonExists()
verifyRefreshButtonExists()
}
}
// The test opens a link in a custom tab then sends it to the browser
@Test
fun openCustomTabInBrowserTest() {
val customTabPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
intentReceiverActivityTestRule.launchActivity(
createCustomTabIntent(
customTabPage.url.toString(),
),
)
customTabScreen {
verifyCustomTabCloseButton()
}.openMainMenu {
}.clickOpenInBrowserButton {
verifyTabCounter("1")
}
}
@Test
fun tabMediaControlButtonTest() {
val audioTestPage = TestAssetHelper.getAudioPageAsset(mockWebServer)

@ -17,7 +17,6 @@ import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.Constants.defaultTopSitesList
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.exitMenu
import org.mozilla.fenix.helpers.TestHelper.getSponsoredShortcutTitle
import org.mozilla.fenix.ui.robots.homeScreen
@ -120,26 +119,6 @@ class SponsoredShortcutsTest {
}
}
// The default search engine should not be displayed as a sponsored shortcut
@Test
fun defaultSearchEngineIsNotDisplayedAsSponsoredShortcutTest() {
val sponsoredShortcutTitle = "Amazon"
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
changeDefaultSearchEngine(defaultSearchEngine)
}
exitMenu()
homeScreen {
verifySponsoredShortcutDoesNotExist(sponsoredShortcutTitle, 2)
verifySponsoredShortcutDoesNotExist(sponsoredShortcutTitle, 3)
}
}
// 1 sponsored shortcut should be displayed if there are 7 pinned top sites
@Test
fun verifySponsoredShortcutsListWithSevenPinnedSitesTest() {

@ -16,6 +16,7 @@ import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.RetryTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.verifyKeyboardVisibility
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
@ -343,10 +344,10 @@ class TabbedBrowsingTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openTabButtonShortcutsMenu {
}.openNewPrivateTabFromShortcutsMenu {
verifyKeyboardVisible()
verifyFocusedNavigationToolbar()
verifyKeyboardVisibility()
verifySearchBarPlaceholder("Search or enter address")
// dismiss search dialog
homeScreen { }.pressBack()
}.dismissSearchBar {
verifyCommonMythsLink()
verifyNavigationToolbar()
}
@ -354,12 +355,26 @@ class TabbedBrowsingTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openTabButtonShortcutsMenu {
}.openTabFromShortcutsMenu {
verifyKeyboardVisible()
verifyFocusedNavigationToolbar()
verifyKeyboardVisibility()
verifySearchBarPlaceholder("Search or enter address")
// dismiss search dialog
homeScreen { }.pressBack()
}.dismissSearchBar {
verifyHomeWordmark()
verifyNavigationToolbar()
}
}
@Test
fun verifySyncedTabsWhenUserIsNotSignedInTest() {
navigationToolbar {
}.openTabTray {
verifySyncedTabsButtonIsSelected(isSelected = false)
clickSyncedTabsButton()
}.toggleToSyncedTabs {
verifySyncedTabsButtonIsSelected(isSelected = true)
verifySyncedTabsListWhenUserIsNotSignedIn()
}.clickSignInToSyncButton {
verifyTurnOnSyncMenu()
}
}
}

@ -118,7 +118,7 @@ class TextSelectionTest {
clickContextMenuItem("Search")
mDevice.waitForIdle()
verifyTabCounter("2")
verifyUrl("google")
verifyUrl("content")
}
}
@ -136,7 +136,7 @@ class TextSelectionTest {
clickContextMenuItem("Private Search")
mDevice.waitForIdle()
verifyTabCounter("2")
verifyUrl("google")
verifyUrl("content")
}
}
@ -214,7 +214,7 @@ class TextSelectionTest {
longClickPageObject(itemContainingText("Crossing"))
clickContextMenuItem("Search")
verifyTabCounter("2")
verifyUrl("google")
verifyUrl("Crossing")
}
}
@ -233,7 +233,7 @@ class TextSelectionTest {
longClickPageObject(itemContainingText("Crossing"))
clickContextMenuItem("Private Search")
verifyTabCounter("2")
verifyUrl("google")
verifyUrl("Crossing")
}
}

@ -68,7 +68,7 @@ class TopSitesTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
expandMenu()
verifyAddToShortcutsButton()
verifyAddToShortcutsButton(shouldExist = true)
}.addToFirefoxHome {
verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
}.goToHomescreen {
@ -85,7 +85,7 @@ class TopSitesTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
expandMenu()
verifyAddToShortcutsButton()
verifyAddToShortcutsButton(shouldExist = true)
}.addToFirefoxHome {
verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
}.goToHomescreen {
@ -112,7 +112,7 @@ class TopSitesTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
expandMenu()
verifyAddToShortcutsButton()
verifyAddToShortcutsButton(shouldExist = true)
}.addToFirefoxHome {
verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
}.goToHomescreen {
@ -135,7 +135,7 @@ class TopSitesTest {
waitForPageToLoad()
}.openThreeDotMenu {
expandMenu()
verifyAddToShortcutsButton()
verifyAddToShortcutsButton(shouldExist = true)
}.addToFirefoxHome {
verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
}.goToHomescreen {
@ -157,7 +157,7 @@ class TopSitesTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
expandMenu()
verifyAddToShortcutsButton()
verifyAddToShortcutsButton(shouldExist = true)
}.addToFirefoxHome {
verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
}.goToHomescreen {
@ -178,7 +178,7 @@ class TopSitesTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
expandMenu()
verifyAddToShortcutsButton()
verifyAddToShortcutsButton(shouldExist = true)
}.addToFirefoxHome {
verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
}.goToHomescreen {
@ -200,7 +200,7 @@ class TopSitesTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
expandMenu()
verifyAddToShortcutsButton()
verifyAddToShortcutsButton(shouldExist = true)
}.addToFirefoxHome {
verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
}.goToHomescreen {

@ -178,55 +178,43 @@ class BrowserRobot {
)
}
fun verifyLinkContextMenuItems(containsURL: Uri, isTheLinkLocal: Boolean = true) {
fun verifyContextMenuForLocalHostLinks(containsURL: Uri) {
// If the link is directing to another local asset the "Download link" option is not available
if (isTheLinkLocal) {
mDevice.waitNotNull(
Until.findObject(By.textContains(containsURL.toString())),
waitingTime,
)
mDevice.waitNotNull(
Until.findObject(text(getStringResource(R.string.mozac_feature_contextmenu_open_link_in_new_tab))),
waitingTime,
)
mDevice.waitNotNull(
Until.findObject(text(getStringResource(R.string.mozac_feature_contextmenu_open_link_in_private_tab))),
waitingTime,
)
mDevice.waitNotNull(
Until.findObject(text(getStringResource(R.string.mozac_feature_contextmenu_copy_link))),
waitingTime,
)
mDevice.waitNotNull(
Until.findObject(text(getStringResource(R.string.mozac_feature_contextmenu_share_link))),
waitingTime,
)
} else {
mDevice.waitNotNull(
Until.findObject(By.textContains(containsURL.toString())),
waitingTime,
)
mDevice.waitNotNull(
Until.findObject(text(getStringResource(R.string.mozac_feature_contextmenu_open_link_in_new_tab))),
waitingTime,
)
mDevice.waitNotNull(
Until.findObject(text(getStringResource(R.string.mozac_feature_contextmenu_open_link_in_private_tab))),
waitingTime,
)
mDevice.waitNotNull(
Until.findObject(text(getStringResource(R.string.mozac_feature_contextmenu_copy_link))),
waitingTime,
)
mDevice.waitNotNull(
Until.findObject(text(getStringResource(R.string.mozac_feature_contextmenu_download_link))),
waitingTime,
)
mDevice.waitNotNull(
Until.findObject(text(getStringResource(R.string.mozac_feature_contextmenu_share_link))),
waitingTime,
)
}
// If the link is not re-directing to an external app the "Open link in external app" option is not available
assertItemContainingTextExists(
contextMenuLinkUrl(containsURL.toString()),
contextMenuOpenLinkInNewTab,
contextMenuOpenLinkInPrivateTab,
contextMenuCopyLink,
contextMenuShareLink,
)
}
fun verifyContextMenuForLinksToOtherApps(containsURL: Uri) {
// If the link is re-directing to an external app the "Open link in external app" option is available
// If the link is not directing to another local asset the "Download link" option is not available
assertItemContainingTextExists(
contextMenuLinkUrl(containsURL.toString()),
contextMenuOpenLinkInNewTab,
contextMenuOpenLinkInPrivateTab,
contextMenuCopyLink,
contextMenuDownloadLink,
contextMenuShareLink,
contextMenuOpenInExternalApp,
)
}
fun verifyContextMenuForLinksToOtherHosts(containsURL: Uri) {
// If the link is re-directing to another host the "Download link" option is available
// If the link is not re-directing to an external app the "Open link in external app" option is not available
assertItemContainingTextExists(
contextMenuLinkUrl(containsURL.toString()),
contextMenuOpenLinkInNewTab,
contextMenuOpenLinkInPrivateTab,
contextMenuCopyLink,
contextMenuDownloadLink,
contextMenuShareLink,
)
}
fun verifyLinkImageContextMenuItems(containsURL: Uri) {
@ -1308,3 +1296,32 @@ private val totalCookieProtectionHintLearnMoreLink =
itemContainingText(getStringResource(R.string.tcp_cfr_learn_more))
private val totalCookieProtectionHintCloseButton =
itemWithDescription(getStringResource(R.string.mozac_cfr_dismiss_button_content_description))
// Context menu items
// Link URL
private fun contextMenuLinkUrl(linkUrl: String) =
itemContainingText(linkUrl)
// Open link in new tab option
private val contextMenuOpenLinkInNewTab =
itemContainingText(getStringResource(R.string.mozac_feature_contextmenu_open_link_in_new_tab))
// Open link in private tab option
private val contextMenuOpenLinkInPrivateTab =
itemContainingText(getStringResource(R.string.mozac_feature_contextmenu_open_link_in_private_tab))
// Copy link option
private val contextMenuCopyLink =
itemContainingText(getStringResource(R.string.mozac_feature_contextmenu_copy_link))
// Download link option
private val contextMenuDownloadLink =
itemContainingText(getStringResource(R.string.mozac_feature_contextmenu_download_link))
// Share link option
private val contextMenuShareLink =
itemContainingText(getStringResource(R.string.mozac_feature_contextmenu_share_link))
// Open in external app option
private val contextMenuOpenInExternalApp =
itemContainingText(getStringResource(R.string.mozac_feature_contextmenu_open_link_in_external_app))

@ -7,16 +7,25 @@
package org.mozilla.fenix.ui.robots
import android.view.View
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsNotSelected
import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.filter
import androidx.compose.ui.test.hasAnyChild
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.hasParent
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.longClick
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onChildren
import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performSemanticsAction
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeLeft
import androidx.compose.ui.test.swipeRight
@ -27,9 +36,16 @@ import androidx.test.espresso.action.GeneralLocation
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.ViewMatchers
import com.google.android.material.bottomsheet.BottomSheetBehavior
import junit.framework.TestCase
import org.hamcrest.Matcher
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.Constants
import org.mozilla.fenix.helpers.HomeActivityComposeTestRule
import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.getStringResource
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.clickAtLocationInView
import org.mozilla.fenix.helpers.idlingresource.BottomSheetBehaviorStateIdlingResource
@ -66,12 +82,30 @@ class ComposeTabDrawerRobot(private val composeTestRule: HomeActivityComposeTest
}
}
fun verifySyncedTabsListWhenUserIsNotSignedIn() {
verifySyncedTabsList()
assertItemContainingTextExists(
itemContainingText(getStringResource(R.string.synced_tabs_sign_in_message)),
itemContainingText(getStringResource(R.string.sync_sign_in)),
itemContainingText(getStringResource(R.string.tab_drawer_fab_sync)),
)
}
fun verifyExistingOpenTabs(vararg titles: String) {
titles.forEach { title ->
itemContainingText(title).waitForExists(waitingTime)
composeTestRule.tabItem(title).assertExists()
}
}
fun verifyNoExistingOpenTabs(vararg titles: String) {
titles.forEach { title ->
TestCase.assertFalse(
itemContainingText(title).waitForExists(TestAssetHelper.waitingTimeShort),
)
}
}
fun verifyNormalTabsList() {
composeTestRule.normalTabsList().assertExists()
}
@ -136,14 +170,10 @@ class ComposeTabDrawerRobot(private val composeTestRule: HomeActivityComposeTest
}
/**
* Verifies a tab with [title] has a close button.
* Verifies a tab's close button when there is only one tab open.
*/
fun verifyTabCloseButton(title: String) {
composeTestRule.tabItem(title).assert(
hasAnyChild(
hasTestTag(TabsTrayTestTag.tabItemClose),
),
)
fun verifyTabCloseButton() {
composeTestRule.closeTabButton().assertExists()
}
fun verifyTabsTrayBehaviorState(expectedState: Int) {
@ -154,6 +184,10 @@ class ComposeTabDrawerRobot(private val composeTestRule: HomeActivityComposeTest
tabsTrayView().check(ViewAssertions.matches(BottomSheetBehaviorHalfExpandedMaxRatioMatcher(0.001f)))
}
fun verifyTabTrayIsOpen() {
composeTestRule.tabsTray().assertExists()
}
fun verifyTabTrayIsClosed() {
composeTestRule.tabsTray().assertDoesNotExist()
}
@ -179,6 +213,80 @@ class ComposeTabDrawerRobot(private val composeTestRule: HomeActivityComposeTest
composeTestRule.tabItem(title).performTouchInput { swipeRight() }
}
/**
* Creates a collection from the provided [tabTitles].
*/
fun createCollection(
vararg tabTitles: String,
collectionName: String,
firstCollection: Boolean = true,
) {
composeTestRule.threeDotButton().performClick()
composeTestRule.dropdownMenuItemSelectTabs().performClick()
for (tab in tabTitles) {
selectTab(tab)
}
clickCollectionsButton(composeTestRule) {
if (!firstCollection) {
clickAddNewCollection()
}
typeCollectionNameAndSave(collectionName)
}
}
/**
* Selects a tab with [title].
*/
@OptIn(ExperimentalTestApi::class)
fun selectTab(title: String) {
composeTestRule.waitUntilExactlyOneExists(hasText(title), TestAssetHelper.waitingTime)
composeTestRule.tabItem(title).performClick()
}
/**
* Performs a long click on a tab with [title].
*/
fun longClickTab(title: String) {
composeTestRule.tabItem(title)
.performTouchInput { longClick(durationMillis = Constants.LONG_CLICK_DURATION) }
}
/**
* Verifies the multi selection counter displays [numOfTabs].
*/
fun verifyTabsMultiSelectionCounter(numOfTabs: Int) {
composeTestRule.multiSelectionCounter()
.assert(hasText("$numOfTabs selected"))
}
/**
* Verifies a tab's media button matches [action] when there is only one tab with media.
*/
fun verifyTabMediaControlButtonState(action: String) {
composeTestRule.tabMediaControlButton(action)
.assertExists()
}
/**
* Clicks a tab's media button when there is only one tab with media.
*/
fun clickTabMediaControlButton(action: String) {
composeTestRule.tabMediaControlButton(action)
.performClick()
}
/**
* Closes a tab with a given [title].
*/
fun closeTabWithTitle(title: String) {
composeTestRule.onAllNodesWithTag(TabsTrayTestTag.tabItemClose)
.filter(hasParent(hasText(title)))
.onFirst()
.performClick()
}
class Transition(private val composeTestRule: HomeActivityComposeTestRule) {
fun openNewTab(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
@ -201,6 +309,19 @@ class ComposeTabDrawerRobot(private val composeTestRule: HomeActivityComposeTest
return Transition(composeTestRule)
}
fun toggleToSyncedTabs(interact: ComposeTabDrawerRobot.() -> Unit): Transition {
composeTestRule.syncedTabsButton().performClick()
ComposeTabDrawerRobot(composeTestRule).interact()
return Transition(composeTestRule)
}
fun clickSignInToSyncButton(interact: SyncSignInRobot.() -> Unit): SyncSignInRobot.Transition {
itemContainingText(getStringResource(R.string.sync_sign_in))
.clickAndWaitForNewWindow(TestAssetHelper.waitingTimeShort)
SyncSignInRobot().interact()
return SyncSignInRobot.Transition()
}
fun openThreeDotMenu(interact: ComposeTabDrawerRobot.() -> Unit): Transition {
composeTestRule.threeDotButton().performClick()
ComposeTabDrawerRobot(composeTestRule).interact()
@ -222,6 +343,24 @@ class ComposeTabDrawerRobot(private val composeTestRule: HomeActivityComposeTest
return BrowserRobot.Transition()
}
fun openPrivateTab(position: Int, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
composeTestRule.privateTabsList()
.onChildren()[position]
.performClick()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun openNormalTab(position: Int, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
composeTestRule.normalTabsList()
.onChildren()[position]
.performClick()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun clickTopBar(interact: ComposeTabDrawerRobot.() -> Unit): Transition {
// The topBar contains other views.
// Don't do the default click in the middle, rather click in some free space - top right.
@ -280,9 +419,48 @@ class ComposeTabDrawerRobot(private val composeTestRule: HomeActivityComposeTest
ComposeTabDrawerRobot(composeTestRule).interact()
return Transition(composeTestRule)
}
fun closeTabDrawer(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
composeTestRule.bannerHandle().performSemanticsAction(SemanticsActions.OnClick)
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun clickSaveCollection(interact: CollectionRobot.() -> Unit): CollectionRobot.Transition {
composeTestRule.collectionsButton().performClick()
CollectionRobot().interact()
return CollectionRobot.Transition()
}
fun clickShareAllTabsButton(interact: ShareOverlayRobot.() -> Unit): ShareOverlayRobot.Transition {
composeTestRule.dropdownMenuItemShareAllTabs().performClick()
ShareOverlayRobot().interact()
return ShareOverlayRobot.Transition()
}
}
}
/**
* Opens a transition in the [ComposeTabDrawerRobot].
*/
fun composeTabDrawer(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
ComposeTabDrawerRobot(composeTestRule).interact()
return ComposeTabDrawerRobot.Transition(composeTestRule)
}
/**
* Clicks on the Collections button in the Tabs Tray banner and opens a transition in the [CollectionRobot].
*/
private fun clickCollectionsButton(composeTestRule: HomeActivityComposeTestRule, interact: CollectionRobot.() -> Unit): CollectionRobot.Transition {
composeTestRule.collectionsButton().performClick()
CollectionRobot().interact()
return CollectionRobot.Transition()
}
/**
* Obtains the root [View] that wraps the Tabs Tray.
*/
@ -341,7 +519,9 @@ private fun ComposeTestRule.emptyPrivateTabsList() = onNodeWithTag(TabsTrayTestT
/**
* Obtains the tab with the provided [title]
*/
private fun ComposeTestRule.tabItem(title: String) = onNodeWithText(title)
private fun ComposeTestRule.tabItem(title: String) = onAllNodesWithTag(TabsTrayTestTag.tabItemRoot)
.filter(hasAnyChild(hasText(title)))
.onFirst()
/**
* Obtains an open tab's close button when there's only one tab open.
@ -392,3 +572,23 @@ private fun ComposeTestRule.dropdownMenuItemTabSettings() = onNodeWithTag(TabsTr
* Obtains the normal tabs counter.
*/
private fun ComposeTestRule.normalTabsCounter() = onNodeWithTag(TabsTrayTestTag.normalTabsCounter)
/**
* Obtains the Tabs Tray banner collections button.
*/
private fun ComposeTestRule.collectionsButton() = onNodeWithTag(TabsTrayTestTag.collectionsButton)
/**
* Obtains the Tabs Tray banner multi selection counter.
*/
private fun ComposeTestRule.multiSelectionCounter() = onNodeWithTag(TabsTrayTestTag.selectionCounter)
/**
* Obtains the Tabs Tray banner handle.
*/
private fun ComposeTestRule.bannerHandle() = onNodeWithTag(TabsTrayTestTag.bannerHandle)
/**
* Obtains the media control button with the given [action] as its content description.
*/
private fun ComposeTestRule.tabMediaControlButton(action: String) = onNodeWithContentDescription(action)

@ -15,18 +15,38 @@ import androidx.test.uiautomator.UiSelector
import junit.framework.TestCase.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithDescriptionExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdAndTextExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdExists
import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.TestHelper.getStringResource
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.TestHelper.waitForObjects
import org.mozilla.fenix.helpers.click
/**
* Implementation of the robot pattern for Custom tabs
*/
class CustomTabRobot {
fun verifyCustomTabsSiteInfoButton() =
assertItemWithResIdExists(
itemWithResId("$packageName:id/mozac_browser_toolbar_security_indicator"),
)
fun verifyCustomTabsShareButton() =
assertItemWithDescriptionExists(
itemWithDescription(getStringResource(R.string.mozac_feature_customtabs_share_link)),
)
fun verifyMainMenuButton() = assertItemWithResIdExists(mainMenuButton)
fun verifyDesktopSiteButtonExists() {
desktopSiteButton().check(matches(isDisplayed()))
}
@ -84,6 +104,12 @@ class CustomTabRobot {
)
}
fun verifyCustomTabUrl(Url: String) {
assertItemWithResIdAndTextExists(
itemWithResIdContainingText("$packageName:id/mozac_browser_toolbar_url_view", Url.drop(7)),
)
}
fun longCLickAndCopyToolbarUrl() {
mDevice.waitForObjects(
mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar")),
@ -106,10 +132,23 @@ class CustomTabRobot {
fun waitForPageToLoad() = progressBar.waitUntilGone(waitingTime)
fun clickCustomTabCloseButton() = closeButton().click()
fun verifyCustomTabActionButton(customTabActionButtonDescription: String) =
assertItemWithDescriptionExists(itemWithDescription(customTabActionButtonDescription))
fun verifyPDFReaderToolbarItems() =
assertItemWithResIdAndTextExists(
itemWithResIdAndText("download", "Download"),
itemWithResIdAndText("openInApp", "Open in app"),
)
class Transition {
fun openMainMenu(interact: CustomTabRobot.() -> Unit): Transition {
mainMenuButton().waitForExists(waitingTime)
mainMenuButton().click()
mainMenuButton.also {
it.waitForExists(waitingTime)
it.click()
}
CustomTabRobot().interact()
return Transition()
@ -122,6 +161,13 @@ class CustomTabRobot {
return BrowserRobot.Transition()
}
fun clickShareButton(interact: ShareOverlayRobot.() -> Unit): ShareOverlayRobot.Transition {
itemWithDescription(getStringResource(R.string.mozac_feature_customtabs_share_link)).click()
ShareOverlayRobot().interact()
return ShareOverlayRobot.Transition()
}
fun goBackToOnboardingScreen(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
mDevice.pressBack()
@ -136,7 +182,7 @@ fun customTabScreen(interact: CustomTabRobot.() -> Unit): CustomTabRobot.Transit
return CustomTabRobot.Transition()
}
private fun mainMenuButton() = mDevice.findObject(UiSelector().description("Menu"))
private val mainMenuButton = itemWithResId("$packageName:id/mozac_browser_toolbar_menu")
private fun desktopSiteButton() = onView(withId(R.id.switch_widget))

@ -24,9 +24,6 @@ import org.hamcrest.Matchers.allOf
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.Constants
import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
@ -63,7 +60,13 @@ class HistoryRobot {
assertVisitedTimeTitle()
}
fun verifyHistoryItemExists(shouldExist: Boolean, item: String) = assertHistoryItemExists(shouldExist, item)
fun verifyHistoryItemExists(shouldExist: Boolean, item: String) {
if (shouldExist) {
assertTrue(mDevice.findObject(UiSelector().textContains(item)).waitForExists(waitingTime))
} else {
assertFalse(mDevice.findObject(UiSelector().textContains(item)).waitForExists(waitingTimeShort))
}
}
fun verifyFirstTestPageTitle(title: String) = assertTestPageTitle(title)
@ -122,36 +125,6 @@ class HistoryRobot {
}
}
fun dismissHistorySearchBarUsingBackButton() {
for (i in 1..Constants.RETRY_COUNT) {
try {
mDevice.pressBack()
assertTrue(
itemWithResId("$packageName:id/mozac_browser_toolbar_edit_url_view")
.waitUntilGone(waitingTime),
)
break
} catch (e: AssertionError) {
if (i == Constants.RETRY_COUNT) {
throw e
}
}
}
}
fun searchForHistoryItem(vararg historyItems: String) {
for (historyItem in historyItems) {
itemWithResId("$packageName:id/mozac_browser_toolbar_edit_url_view").also {
it.waitForExists(waitingTime)
it.setText(historyItem)
}
mDevice.waitForWindowUpdate(packageName, waitingTimeShort)
}
}
fun verifySearchedHistoryItemExists(historyItemUrl: String, exists: Boolean = true) =
assertItemContainingTextExists(itemContainingText(historyItemUrl), exists = exists)
fun openSearchGroup(searchTerm: String) {
mDevice.findObject(UiSelector().text(searchTerm)).waitForExists(waitingTime)
mDevice.findObject(UiSelector().text(searchTerm)).click()
@ -225,14 +198,6 @@ private fun assertEmptyHistoryView() =
private fun assertHistoryListExists() =
mDevice.findObject(UiSelector().resourceId("$packageName:id/history_list")).waitForExists(waitingTime)
private fun assertHistoryItemExists(shouldExist: Boolean, item: String) {
if (shouldExist) {
assertTrue(mDevice.findObject(UiSelector().textContains(item)).waitForExists(waitingTime))
} else {
assertFalse(mDevice.findObject(UiSelector().textContains(item)).waitForExists(waitingTimeShort))
}
}
private fun assertVisitedTimeTitle() =
onView(withId(R.id.header_title)).check(matches(withText("Today")))

@ -6,7 +6,6 @@
package org.mozilla.fenix.ui.robots
import android.graphics.Bitmap
import android.view.View
import android.widget.EditText
import android.widget.TextView
@ -35,7 +34,6 @@ import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withHint
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.uiautomator.By
@ -44,7 +42,6 @@ import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import androidx.test.uiautomator.Until.findObject
import mozilla.components.browser.state.state.searchEngines
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.instanceOf
@ -53,7 +50,6 @@ import org.junit.Assert
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.Constants.LISTS_MAXSWIPES
import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION
import org.mozilla.fenix.helpers.HomeActivityComposeTestRule
@ -71,7 +67,6 @@ import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndDescription
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
import org.mozilla.fenix.helpers.TestHelper.appContext
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.TestHelper.getStringResource
import org.mozilla.fenix.helpers.TestHelper.mDevice
@ -79,7 +74,6 @@ import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.helpers.withBitmapDrawable
import org.mozilla.fenix.tabstray.TabsTrayTestTag
import org.mozilla.fenix.utils.Settings
@ -94,7 +88,7 @@ class HomeScreenRobot {
" else who uses this device."
fun verifyNavigationToolbar() = assertItemWithResIdExists(navigationToolbar)
fun verifyFocusedNavigationToolbar() = assertFocusedNavigationToolbar()
fun verifyHomeScreen() = assertItemWithResIdExists(homeScreen)
fun verifyPrivateBrowsingHomeScreen() {
@ -208,10 +202,9 @@ class HomeScreenRobot {
assertItemWithResIdExists(homepageWordmark)
}
fun verifyHomeComponent() = assertHomeComponent()
fun verifyDefaultSearchEngine(searchEngine: String) = verifySearchEngineIcon(searchEngine)
fun verifyTabCounter(numberOfOpenTabs: String) =
assertItemWithResIdAndTextExists(tabCounter(numberOfOpenTabs))
fun verifyKeyboardVisible() = assertKeyboardVisibility(isExpectedToBeVisible = true)
fun verifyWallpaperImageApplied(isEnabled: Boolean) {
if (isEnabled) {
@ -784,6 +777,14 @@ class HomeScreenRobot {
return TabDrawerRobot.Transition()
}
fun clickSaveTabsToCollectionButton(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
scrollToElementByText(getStringResource(R.string.no_collections_description2))
saveTabsToCollectionButton().click()
ComposeTabDrawerRobot(composeTestRule).interact()
return ComposeTabDrawerRobot.Transition(composeTestRule)
}
fun expandCollection(title: String, interact: CollectionRobot.() -> Unit): CollectionRobot.Transition {
assertItemContainingTextExists(itemContainingText(title))
itemContainingText(title).clickAndWaitForNewWindow(waitingTimeShort)
@ -827,6 +828,17 @@ class HomeScreenRobot {
return TabDrawerRobot.Transition()
}
fun clickJumpBackInShowAllButton(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
mDevice
.findObject(
UiSelector()
.textContains(getStringResource(R.string.recent_tabs_show_all)),
).clickAndWaitForNewWindow(waitingTime)
ComposeTabDrawerRobot(composeTestRule).interact()
return ComposeTabDrawerRobot.Transition(composeTestRule)
}
fun clickJumpBackInItemWithTitle(itemTitle: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice
.findObject(
@ -911,10 +923,6 @@ private fun assertKeyboardVisibility(isExpectedToBeVisible: Boolean) =
.contains("mInputShown=true"),
)
private fun assertFocusedNavigationToolbar() =
onView(allOf(withHint("Search or enter address")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertTabButton() =
onView(allOf(withId(R.id.tab_button), isDisplayed()))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
@ -939,23 +947,6 @@ private fun assertHomeComponent() =
private fun threeDotButton() = onView(allOf(withId(R.id.menuButton)))
private fun verifySearchEngineIcon(searchEngineIcon: Bitmap, searchEngineName: String) {
onView(withId(R.id.search_engine_icon))
.check(matches(withBitmapDrawable(searchEngineIcon, searchEngineName)))
}
private fun getSearchEngine(searchEngineName: String) =
appContext.components.core.store.state.search.searchEngines.find { it.name == searchEngineName }
private fun verifySearchEngineIcon(searchEngineName: String) {
val defaultSearchEngine = getSearchEngine(searchEngineName)
?: throw AssertionError("No search engine with name $searchEngineName")
verifySearchEngineIcon(defaultSearchEngine.icon, defaultSearchEngine.name)
}
private fun collectionTitle(title: String, rule: ComposeTestRule) =
rule.onNode(hasText(title))
private fun assertExistingTopSitesList() =
onView(allOf(withId(R.id.top_sites_list)))
.check((matches(withEffectiveVisibility(Visibility.VISIBLE))))

@ -6,6 +6,7 @@ package org.mozilla.fenix.ui.robots
import android.net.Uri
import android.widget.TextView
import androidx.compose.ui.test.onNodeWithTag
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
@ -19,12 +20,14 @@ import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import org.hamcrest.Matchers.allOf
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.HomeActivityComposeTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.tabstray.TabsTrayTestTag
/*
* Implementation of Robot Pattern for the multiple selection toolbar of History and Bookmarks menus.
@ -110,6 +113,14 @@ class LibrarySubMenusMultipleSelectionToolbarRobot {
return TabDrawerRobot.Transition()
}
fun clickOpenNewTab(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
openInNewTabButton().click()
composeTestRule.onNodeWithTag(TabsTrayTestTag.tabsTray).assertExists()
ComposeTabDrawerRobot(composeTestRule).interact()
return ComposeTabDrawerRobot.Transition(composeTestRule)
}
fun clickOpenPrivateTab(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
openInPrivateTabButton().click()
mDevice.waitNotNull(
@ -120,6 +131,13 @@ class LibrarySubMenusMultipleSelectionToolbarRobot {
TabDrawerRobot().interact()
return TabDrawerRobot.Transition()
}
fun clickOpenPrivateTab(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
openInPrivateTabButton().click()
ComposeTabDrawerRobot(composeTestRule).interact()
return ComposeTabDrawerRobot.Transition(composeTestRule)
}
}
}

@ -8,6 +8,7 @@ package org.mozilla.fenix.ui.robots
import android.net.Uri
import android.os.Build
import androidx.compose.ui.test.onNodeWithTag
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
@ -32,7 +33,9 @@ import org.hamcrest.CoreMatchers.not
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.Constants
import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION
import org.mozilla.fenix.helpers.HomeActivityComposeTestRule
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText
import org.mozilla.fenix.helpers.SessionLoadedIdlingResource
@ -41,8 +44,10 @@ import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
import org.mozilla.fenix.helpers.TestHelper.getStringResource
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.TestHelper.waitForObjects
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.tabstray.TabsTrayTestTag
/**
* Implementation of Robot Pattern for the URL toolbar.
@ -117,6 +122,24 @@ class NavigationToolbarRobot {
getStringResource(R.string.search_hint),
)
// New unified search UI selector
fun verifySearchBarPlaceholder(text: String) {
urlBar().waitForExists(waitingTime)
assertTrue(
urlBar().text == text,
)
}
// New unified search UI selector
fun verifyDefaultSearchEngine(engineName: String) =
assertTrue(
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/search_selector")
.childSelector(UiSelector().description(engineName)),
).waitForExists(waitingTime),
)
fun verifyTextSelectionOptions(vararg textSelectionOptions: String) {
for (textSelectionOption in textSelectionOptions) {
mDevice.waitNotNull(Until.findObject(textContains(textSelectionOption)), waitingTime)
@ -208,6 +231,37 @@ class NavigationToolbarRobot {
return TabDrawerRobot.Transition()
}
fun openComposeTabDrawer(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
for (i in 1..Constants.RETRY_COUNT) {
try {
mDevice.waitForObjects(
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/mozac_browser_toolbar_browser_actions"),
),
waitingTime,
)
tabTrayButton().click()
composeTestRule.onNodeWithTag(TabsTrayTestTag.tabsTray).assertExists()
break
} catch (e: AssertionError) {
if (i == Constants.RETRY_COUNT) {
throw e
} else {
mDevice.waitForIdle()
}
}
}
composeTestRule.onNodeWithTag(TabsTrayTestTag.fab).assertExists()
ComposeTabDrawerRobot(composeTestRule).interact()
return ComposeTabDrawerRobot.Transition(composeTestRule)
}
fun visitLinkFromClipboard(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
if (clearAddressBarButton().waitForExists(waitingTimeShort)) {
clearAddressBarButton().click()
@ -257,7 +311,7 @@ class NavigationToolbarRobot {
return NavigationToolbarRobot.Transition()
}
fun openTabFromShortcutsMenu(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
fun openTabFromShortcutsMenu(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
mDevice.waitForIdle(waitingTime)
onView(withId(R.id.mozac_browser_menu_recyclerView))
@ -270,11 +324,11 @@ class NavigationToolbarRobot {
),
)
HomeScreenRobot().interact()
return HomeScreenRobot.Transition()
SearchRobot().interact()
return SearchRobot.Transition()
}
fun openNewPrivateTabFromShortcutsMenu(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
fun openNewPrivateTabFromShortcutsMenu(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
mDevice.waitForIdle(waitingTime)
onView(withId(R.id.mozac_browser_menu_recyclerView))
@ -287,8 +341,8 @@ class NavigationToolbarRobot {
),
)
HomeScreenRobot().interact()
return HomeScreenRobot.Transition()
SearchRobot().interact()
return SearchRobot.Transition()
}
fun clickUrlbar(interact: SearchRobot.() -> Unit): SearchRobot.Transition {

@ -10,12 +10,15 @@ import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.helpers.Constants.RETRY_COUNT
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdAndTextExists
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
import kotlin.AssertionError
class NotificationRobot {
@ -67,18 +70,22 @@ class NotificationRobot {
}
fun clickDownloadNotificationControlButton(action: String) {
try {
assertTrue(downloadSystemNotificationButton(action).waitForExists(waitingTimeShort))
} catch (e: AssertionError) {
notificationTray().flingToEnd(1)
}
for (i in 1..RETRY_COUNT) {
try {
assertItemWithResIdAndTextExists(downloadSystemNotificationButton(action))
downloadSystemNotificationButton(action).clickAndWaitForNewWindow(waitingTimeShort)
assertItemWithResIdAndTextExists(
downloadSystemNotificationButton(action),
exists = false,
)
downloadSystemNotificationButton(action).click()
// API 30 Bug? Sometimes a click doesn't register, try again
try {
assertTrue(downloadSystemNotificationButton(action).waitUntilGone(waitingTime))
} catch (e: AssertionError) {
downloadSystemNotificationButton(action).click()
break
} catch (e: AssertionError) {
if (i == RETRY_COUNT) {
throw e
}
mDevice.waitForWindowUpdate(packageName, waitingTimeShort)
}
}
}

@ -6,16 +6,9 @@
package org.mozilla.fenix.ui.robots
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.junit4.android.ComposeNotIdleException
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToIndex
import androidx.compose.ui.test.performScrollToNode
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
@ -24,14 +17,12 @@ import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiSelector
import org.hamcrest.CoreMatchers.allOf
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
@ -42,7 +33,6 @@ import org.mozilla.fenix.helpers.Constants.SPEECH_RECOGNITION
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdExists
import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
import org.mozilla.fenix.helpers.SessionLoadedIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
@ -52,7 +42,6 @@ import org.mozilla.fenix.helpers.TestHelper.isPackageInstalled
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.TestHelper.waitForObjects
import org.mozilla.fenix.helpers.click
/**
* Implementation of Robot Pattern for the search fragment.
@ -71,7 +60,17 @@ class SearchRobot {
exists = isDisplayed,
)
fun verifyScanButton() = assertScanButton()
fun verifyScanButtonVisibility(visible: Boolean = true) {
if (visible) {
assertTrue(
scanButton.waitForExists(waitingTime),
)
} else {
assertTrue(
scanButton.waitUntilGone(waitingTime),
)
}
}
fun verifyVoiceSearchButtonVisibility(enabled: Boolean) {
if (enabled) {
@ -91,8 +90,6 @@ class SearchRobot {
}
}
fun verifySearchEngineButton() = assertSearchButton()
fun verifySearchEngineSuggestionResults(rule: ComposeTestRule, searchSuggestion: String) {
rule.waitForIdle()
for (i in 1..RETRY_COUNT) {
@ -188,40 +185,44 @@ class SearchRobot {
).click()
}
fun verifySearchEnginePrompt(rule: ComposeTestRule, searchEngineName: String) =
assertSearchEnginePrompt(rule, searchEngineName)
fun verifySearchBarEmpty() = assertSearchBarEmpty()
fun verifyKeyboardVisibility() = assertKeyboardVisibility(isExpectedToBeVisible = true)
fun verifySearchEngineList(rule: ComposeTestRule) = rule.assertSearchEngineList()
fun verifySearchSelectorButton() {
assertTrue(itemWithResId("$packageName:id/search_selector").waitForExists(waitingTime))
assertTrue(searchSelectorButton.waitForExists(waitingTime))
}
fun clickSearchSelectorButton() {
searchSelectorButton.waitForExists(waitingTime)
searchSelectorButton.click()
}
fun verifySearchEngineIcon(name: String) =
assertTrue(itemWithDescription(name).waitForExists(waitingTime))
fun verifySearchBarPlaceholder(text: String) {
browserToolbarEditView().waitForExists(waitingTime)
assertTrue(
itemWithResIdAndText("$packageName:id/mozac_browser_toolbar_edit_url_view", text)
.waitForExists(waitingTime),
browserToolbarEditView().text == text,
)
}
fun verifyDefaultSearchEngine(expectedText: String) = assertDefaultSearchEngine(expectedText)
fun verifyEnginesListShortcutContains(rule: ComposeTestRule, searchEngineName: String) = assertEngineListShortcutContains(rule, searchEngineName)
fun changeDefaultSearchEngine(rule: ComposeTestRule, searchEngineName: String) =
rule.selectDefaultSearchEngine(searchEngineName)
fun clickSearchEngineShortcutButton() {
itemWithResId("$packageName:id/search_engines_shortcut_button").also {
it.waitForExists(waitingTime)
it.click()
fun verifySearchShortcutListContains(vararg searchEngineName: String, shouldExist: Boolean = true) {
searchEngineName.forEach {
if (shouldExist) {
assertTrue(
searchShortcutList.getChild(UiSelector().text(it))
.waitForExists(waitingTimeShort),
)
} else {
assertTrue(
searchShortcutList.getChild(UiSelector().text(it))
.waitUntilGone(waitingTimeShort),
)
}
}
mDevice.waitForIdle(waitingTimeShort)
}
// New unified search UI search selector.
fun selectTemporarySearchMethod(searchEngineName: String) {
searchShortcutList.getChild(UiSelector().text(searchEngineName)).click()
}
fun clickScanButton() =
@ -243,7 +244,10 @@ class SearchRobot {
fun verifyScannerOpen() {
assertTrue(
mDevice.findObject(UiSelector().resourceId("$packageName:id/view_finder"))
.waitForExists(waitingTime),
.waitForExists(waitingTime) ||
// In case there is no camera available, an error will be shown.
mDevice.findObject(UiSelector().resourceId("$packageName:id/camera_error"))
.exists(),
)
}
@ -257,20 +261,6 @@ class SearchRobot {
mDevice.waitForIdle()
}
@OptIn(ExperimentalTestApi::class)
fun scrollToSearchEngineSettings(rule: ComposeTestRule) {
// Soft keyboard is visible on screen on view access; hide it
onView(allOf(withId(R.id.search_wrapper))).perform(
closeSoftKeyboard(),
)
mDevice.findObject(UiSelector().text("Google"))
.waitForExists(waitingTime)
rule.onNodeWithTag("mozac.awesomebar.suggestions")
.performScrollToIndex(5)
}
fun clickClearButton() {
clearButton().click()
}
@ -297,36 +287,15 @@ class SearchRobot {
pasteText.click()
}
fun clickSearchEnginePrompt(rule: ComposeTestRule, searchEngineName: String) =
rule.onNodeWithText("Search $searchEngineName").performClick()
fun expandSearchSuggestionsList() {
onView(allOf(withId(R.id.search_wrapper))).perform(
closeSoftKeyboard(),
)
awesomeBar.swipeUp(2)
browserToolbarEditView().swipeUp(2)
}
fun verifyTranslatedFocusedNavigationToolbar(toolbarHintString: String) =
assertTranslatedFocusedNavigationToolbar(toolbarHintString)
fun verifySearchEngineShortcuts(rule: ComposeTestRule, vararg searchEngines: String) {
for (searchEngine in searchEngines) {
rule.waitForIdle()
rule.onNodeWithText(searchEngine).assertIsDisplayed()
}
}
fun verifySearchEngineShortcutsAreNotDisplayed(rule: ComposeTestRule, vararg searchEngines: String) {
mDevice.findObject(
UiSelector().resourceId("$packageName:id/pill_wrapper_divider"),
).waitForExists(waitingTime)
for (searchEngine in searchEngines) {
rule.waitForIdle()
rule.onNodeWithText(searchEngine).assertDoesNotExist()
}
}
assertTrue(browserToolbarEditView().text.contains(toolbarHintString))
fun verifyTypedToolbarText(expectedText: String) {
mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
@ -397,11 +366,8 @@ class SearchRobot {
return BrowserRobot.Transition()
}
fun clickSearchEngineSettings(
rule: ComposeTestRule,
interact: SettingsSubMenuSearchRobot.() -> Unit,
): SettingsSubMenuSearchRobot.Transition {
rule.onNodeWithText("Search engine settings").performClick()
fun clickSearchEngineSettings(interact: SettingsSubMenuSearchRobot.() -> Unit): SettingsSubMenuSearchRobot.Transition {
searchShortcutList.getChild(UiSelector().text("Search settings")).click()
SettingsSubMenuSearchRobot().interact()
return SettingsSubMenuSearchRobot.Transition()
@ -419,6 +385,11 @@ class SearchRobot {
}
}
fun searchScreen(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
SearchRobot().interact()
return SearchRobot.Transition()
}
private fun browserToolbarEditView() =
mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_edit_url_view"))
@ -428,143 +399,16 @@ private val dismissPermissionButton =
private val goToPermissionsSettingsButton =
mDevice.findObject(UiSelector().text("GO TO SETTINGS"))
private val scanButton =
itemWithResIdAndText(
"$packageName:id/qr_scan_button",
getStringResource(R.string.search_scan_button),
)
private val scanButton = itemWithDescription("Scan")
private fun clearButton() =
mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_clear_view"))
private fun searchWrapper() = mDevice.findObject(UiSelector().resourceId("$packageName:id/search_wrapper"))
private fun assertSearchEnginePrompt(rule: ComposeTestRule, searchEngineName: String) {
rule.waitForIdle()
rule.onNodeWithText("Search $searchEngineName").assertIsDisplayed()
rule.onNodeWithText(
getStringResource(R.string.search_engine_suggestions_description),
).assertIsDisplayed()
}
private fun assertScanButton() =
assertTrue(
scanButton.waitForExists(waitingTime),
)
private fun assertSearchButton() =
assertTrue(
mDevice.findObject(
UiSelector().resourceId("$packageName:id/search_engines_shortcut_button"),
).waitForExists(waitingTime),
)
private fun assertSearchBarEmpty() =
assertTrue(
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/mozac_browser_toolbar_edit_url_view")
.textContains(""),
).waitForExists(waitingTime),
)
fun searchScreen(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
SearchRobot().interact()
return SearchRobot.Transition()
}
private fun assertKeyboardVisibility(isExpectedToBeVisible: Boolean): () -> Unit = {
searchWrapper().waitForExists(waitingTime)
assertEquals(
"Keyboard not shown",
isExpectedToBeVisible,
mDevice
.executeShellCommand("dumpsys input_method | grep mInputShown")
.contains("mInputShown=true"),
)
}
private fun ComposeTestRule.assertSearchEngineList() {
onView(withId(R.id.mozac_browser_toolbar_edit_icon)).click()
onNodeWithText("Google")
.assertExists()
.assertIsDisplayed()
onNodeWithText("Amazon.com")
.assertExists()
.assertIsDisplayed()
private val searchSelectorButton = itemWithResId("$packageName:id/search_selector")
onNodeWithText("Bing")
.assertExists()
.assertIsDisplayed()
onNodeWithText("DuckDuckGo")
.assertExists()
.assertIsDisplayed()
onNodeWithText("Wikipedia")
.assertExists()
.assertIsDisplayed()
onNodeWithText("eBay")
.assertExists()
.assertIsDisplayed()
}
@OptIn(ExperimentalTestApi::class)
private fun assertEngineListShortcutContains(rule: ComposeTestRule, searchEngineName: String) {
try {
rule.waitForIdle()
} catch (e: ComposeNotIdleException) {
mDevice.pressBack()
navigationToolbar {
}.clickUrlbar {
clickSearchEngineShortcutButton()
}
} finally {
mDevice.findObject(
UiSelector().textContains("Google"),
).waitForExists(waitingTime)
rule.onNodeWithTag("mozac.awesomebar.suggestions")
.performScrollToNode(hasText(searchEngineName))
rule.onNodeWithText(searchEngineName)
.assertIsDisplayed()
.assertHasClickAction()
}
}
private fun ComposeTestRule.selectDefaultSearchEngine(searchEngine: String) {
onNodeWithText(searchEngine)
.assertExists()
.assertIsDisplayed()
.performClick()
}
private fun assertDefaultSearchEngine(expectedText: String) =
assertTrue(
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/mozac_browser_toolbar_edit_icon")
.descriptionContains(expectedText),
).waitForExists(waitingTime),
)
private fun assertTranslatedFocusedNavigationToolbar(toolbarHintString: String) =
assertTrue(
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/mozac_browser_toolbar_edit_url_view")
.textContains(toolbarHintString),
).waitForExists(waitingTime),
)
private val awesomeBar =
mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_edit_url_view"))
private val searchShortcutList =
mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_menu_recyclerView"))
private val voiceSearchButton = mDevice.findObject(UiSelector().description("Voice search"))
private fun goBackButton() = onView(allOf(withContentDescription("Navigate up")))

@ -4,8 +4,6 @@
package org.mozilla.fenix.ui.robots
import androidx.appcompat.widget.AppCompatTextView
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.action.ViewActions
@ -19,10 +17,12 @@ import androidx.test.espresso.matcher.ViewMatchers.withHint
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.containsString
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists
@ -31,6 +31,7 @@ import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdExists
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.getStringResource
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
@ -107,12 +108,18 @@ class SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot {
fun clickLastUsedSortingOption() =
itemContainingText(getStringResource(R.string.saved_logins_sort_strategy_last_used)).click()
fun verifySortedLogin(testRule: HomeActivityIntentTestRule, position: Int, loginTitle: String) {
val list = testRule.activity.findViewById<RecyclerView>(R.id.saved_logins_list)
val item = list.layoutManager?.findViewByPosition(position)
val title = item?.findViewById<AppCompatTextView>(R.id.webAddressView)
assertEquals(loginTitle, title?.text)
}
fun verifySortedLogin(position: Int, loginTitle: String) =
assertTrue(
mDevice.findObject(
UiSelector()
.className("android.view.ViewGroup")
.index(position),
).getChild(
UiSelector()
.resourceId("$packageName:id/webAddressView")
.textContains(loginTitle),
).waitForExists(waitingTime),
)
fun searchLogin(searchTerm: String) =
itemContainingText(getStringResource(R.string.preferences_passwords_saved_logins_search)).setText(searchTerm)
@ -156,16 +163,39 @@ class SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot {
fun saveEditedLogin() = itemWithResId("$packageName:id/save_login_button").click()
fun verifySaveLoginButtonIsEnabled(isEnabled: Boolean) {
if (isEnabled) {
assertTrue(itemWithResId("$packageName:id/save_login_button").isChecked)
} else {
assertFalse(itemWithResId("$packageName:id/save_login_button").isChecked)
}
}
fun revealPassword() = onView(withId(R.id.revealPasswordButton)).click()
fun verifyPasswordSaved(password: String) =
onView(withId(R.id.passwordText)).check(matches(withText(password)))
fun verifyUserNameRequiredErrorMessage() =
assertItemContainingTextExists(itemContainingText(getStringResource(R.string.saved_login_username_required)))
fun verifyPasswordRequiredErrorMessage() =
assertItemContainingTextExists(itemContainingText(getStringResource(R.string.saved_login_password_required)))
fun clickGoBackButton() = goBackButton().click()
fun clickCopyUserNameButton() =
itemWithResId("$packageName:id/copyUsername").also {
it.waitForExists(waitingTime)
it.click()
}
fun clickCopyPasswordButton() =
itemWithResId("$packageName:id/copyPassword").also {
it.waitForExists(waitingTime)
it.click()
}
class Transition {
fun goBack(interact: SettingsSubMenuLoginsAndPasswordRobot.() -> Unit): SettingsSubMenuLoginsAndPasswordRobot.Transition {
goBackButton().perform(ViewActions.click())

@ -6,205 +6,264 @@
package org.mozilla.fenix.ui.robots
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasAnySibling
import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.closeSoftKeyboard
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions.clearText
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withChild
import androidx.test.espresso.matcher.ViewMatchers.withClassName
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withParent
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiSelector
import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.not
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.endsWith
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
import org.mozilla.fenix.helpers.TestHelper.getAvailableSearchEngines
import org.mozilla.fenix.helpers.TestHelper.getRegionSearchEnginesList
import org.mozilla.fenix.helpers.TestHelper.getStringResource
import org.mozilla.fenix.helpers.TestHelper.hasCousin
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.isChecked
import org.mozilla.fenix.helpers.isEnabled
/**
* Implementation of Robot Pattern for the settings search sub menu.
*/
class SettingsSubMenuSearchRobot {
fun verifySearchToolbar() = assertSearchToolbar()
fun verifyDefaultSearchEngineHeader() = assertDefaultSearchEngineHeader()
fun verifySearchEngineList() = assertSearchEngineList()
fun verifyShowSearchSuggestions() {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText("Show search suggestions")),
fun verifyToolbarText(title: String) {
onView(
allOf(
withId(R.id.navigationToolbar),
hasDescendant(withContentDescription(R.string.action_bar_up_description)),
hasDescendant(withText(title)),
),
)
onView(withText("Show search suggestions"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
fun verifyShowSearchShortcuts() = assertShowSearchShortcuts()
fun verifySearchEnginesSectionHeader() {
onView(withText("Search Engines")).check(matches(isDisplayed()))
}
fun verifyShowClipboardSuggestionsDefault() {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText(getStringResource(R.string.preferences_show_clipboard_suggestions))),
),
)
onView(withText(getStringResource(R.string.preferences_show_clipboard_suggestions)))
fun verifyDefaultSearchEngineHeader() {
defaultSearchEngineHeader
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(true)))))
}
fun toggleClipboardSuggestion() {
onView(withText(getStringResource(R.string.preferences_show_clipboard_suggestions)))
.click()
fun verifyDefaultSearchEngineSummary(engineName: String) {
defaultSearchEngineHeader.check(matches(hasSibling(withText(engineName))))
}
fun verifySearchBrowsingHistory() {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText("Search browsing history")),
),
)
searchHistoryToggle
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
fun verifyManageSearchShortcutsHeader() {
manageSearchShortcutsHeader.check(matches(isDisplayed()))
}
fun verifySearchBookmarks() {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText("Search bookmarks")),
),
)
searchBookmarksToggle
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
fun verifyManageShortcutsSummary() {
manageSearchShortcutsHeader
.check(matches(hasSibling(withText("Edit engines visible in the search menu"))))
}
fun changeDefaultSearchEngine(searchEngineName: String) =
selectSearchEngine(searchEngineName)
fun verifyEnginesShortcutsListHeader() {
assertTrue(itemWithText("Engines visible on the search menu").exists())
}
fun toggleAutocomplete() {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText(getStringResource(R.string.preferences_enable_autocomplete_urls))),
),
)
fun verifyAddressBarSectionHeader() {
onView(withText("Address bar")).check(matches(isDisplayed()))
}
onView(withText(getStringResource(R.string.preferences_enable_autocomplete_urls)))
.click()
fun verifyDefaultSearchEngineList() {
defaultSearchEngineOption("Google")
.check(matches(hasSibling(withId(R.id.engine_icon))))
.check(matches(isDisplayed()))
defaultSearchEngineOption("Bing")
.check(matches(hasSibling(withId(R.id.engine_icon))))
.check(matches(isDisplayed()))
defaultSearchEngineOption("DuckDuckGo")
.check(matches(hasSibling(withId(R.id.engine_icon))))
.check(matches(isDisplayed()))
assertTrue(addSearchEngineButton.exists())
}
fun toggleShowSearchSuggestions() {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText("Show search suggestions")),
),
)
fun verifyManageShortcutsList(testRule: ComposeTestRule) {
val availableShortcutsEngines = getRegionSearchEnginesList() + getAvailableSearchEngines()
onView(withText("Show search suggestions"))
.perform(click())
availableShortcutsEngines.forEach {
testRule.onNodeWithText(it.name)
.assert(hasAnySibling(hasContentDescription("${it.name} search engine")))
.assertIsDisplayed()
}
assertTrue(addSearchEngineButton.exists())
}
fun toggleShowSearchShortcuts() =
itemContainingText(getStringResource(R.string.preferences_show_search_engines))
.also {
it.waitForExists(waitingTimeShort)
it.click()
/**
* Method that verifies the selected engines inside the Manage search shortcuts list.
*/
fun verifySearchShortcutChecked(vararg engineShortcut: EngineShortcut) {
engineShortcut.forEach {
val shortcutIsChecked = mDevice.findObject(UiSelector().text(it.name))
.getFromParent(
UiSelector().index(it.checkboxIndex),
).isChecked
if (it.isChecked) {
assertTrue(shortcutIsChecked)
} else {
assertFalse(shortcutIsChecked)
}
}
}
fun toggleVoiceSearch() {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText("Show voice search")),
),
)
onView(withText("Show voice search")).perform(click())
fun verifyAutocompleteURlsIsEnabled(enabled: Boolean) {
autocompleteSwitchButton()
.check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
}
fun switchSearchHistoryToggle() {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText("Search browsing history")),
),
)
searchHistoryToggle.click()
fun verifyShowSearchSuggestionsEnabled(enabled: Boolean) {
showSearchSuggestionSwitchButton()
.check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
}
fun switchSearchBookmarksToggle() {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText("Search bookmarks")),
),
)
searchBookmarksToggle.click()
fun verifyShowSearchSuggestionsInPrivateEnabled(enabled: Boolean) {
showSuggestionsInPrivateModeSwitch()
.check(
matches(
hasSibling(
withChild(
allOf(
withClassName(endsWith("CheckBox")),
isChecked(enabled),
),
),
),
),
)
}
fun toggleShowSuggestionsInPrivateSessions() {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText(getStringResource(R.string.preferences_show_search_suggestions_in_private))),
),
)
onView(withText(getStringResource(R.string.preferences_show_search_suggestions_in_private)))
fun verifyShowClipboardSuggestionsEnabled(enabled: Boolean) {
showClipboardSuggestionSwitch()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
}
fun verifySearchBrowsingHistoryEnabled(enabled: Boolean) {
searchHistorySwitchButton()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
}
fun verifySearchBookmarksEnabled(enabled: Boolean) {
searchBookmarksSwitchButton()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
}
fun verifySearchSyncedTabsEnabled(enabled: Boolean) {
searchSyncedTabsSwitchButton()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
}
fun verifyVoiceSearchEnabled(enabled: Boolean) {
voiceSearchSwitchButton()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
}
fun openDefaultSearchEngineMenu() {
defaultSearchEngineHeader.click()
}
fun openManageShortcutsMenu() {
manageSearchShortcutsHeader.click()
}
fun changeDefaultSearchEngine(searchEngineName: String) {
onView(withText(searchEngineName))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.perform(click())
}
fun selectSearchShortcut(shortcut: EngineShortcut) {
mDevice.findObject(UiSelector().text(shortcut.name))
.getFromParent(UiSelector().index(shortcut.checkboxIndex))
.click()
}
fun openAddSearchEngineMenu() = addSearchEngineButton().click()
fun toggleAutocomplete() = autocompleteSwitchButton().click()
fun toggleShowSearchSuggestions() = showSearchSuggestionSwitchButton().click()
fun toggleVoiceSearch() {
voiceSearchSwitchButton().perform(click())
}
fun verifyAddSearchEngineList() = assertAddSearchEngineList()
fun toggleClipboardSuggestion() {
showClipboardSuggestionSwitch().click()
}
fun verifyEngineListContains(searchEngineName: String) = assertEngineListContains(searchEngineName)
fun switchSearchHistoryToggle() = searchHistorySwitchButton().click()
fun verifyEngineListDoesNotContain(searchEngineName: String) = assertEngineListDoesNotContain(searchEngineName)
fun switchSearchBookmarksToggle() = searchBookmarksSwitchButton().click()
fun verifyDefaultSearchEngine(searchEngineName: String) = assertDefaultSearchEngine(searchEngineName)
fun switchShowSuggestionsInPrivateSessionsToggle() =
showSuggestionsInPrivateModeSwitch().click()
fun verifyThreeDotButtonIsNotDisplayed(searchEngineName: String) = assertThreeDotButtonIsNotDisplayed(searchEngineName)
fun openAddSearchEngineMenu() = addSearchEngineButton.click()
fun verifyAddSearchEngineListContains(vararg searchEngines: String) {
for (searchEngine in searchEngines) {
assertEngineListContains(searchEngine)
fun verifyEngineListContains(searchEngineName: String, shouldExist: Boolean) {
if (shouldExist) {
assertTrue(itemWithText(searchEngineName).waitForExists(waitingTimeShort))
} else {
assertFalse(itemWithText(searchEngineName).waitForExists(waitingTimeShort))
}
}
fun saveNewSearchEngine() {
addSearchEngineSaveButton().click()
assertTrue(
mDevice.findObject(
UiSelector().textContains("Default search engine"),
).waitForExists(waitingTime),
)
fun verifyDefaultSearchEngineSelected(searchEngineName: String) {
defaultSearchEngineOption(searchEngineName).check(matches(isChecked(true)))
}
fun addNewSearchEngine(searchEngineName: String) {
selectSearchEngine(searchEngineName)
saveNewSearchEngine()
fun verifySaveSearchEngineButtonEnabled(enabled: Boolean) {
addSearchEngineSaveButton().check(matches(isEnabled(enabled)))
}
fun selectAddCustomSearchEngine() = onView(withText("Other")).click()
fun saveNewSearchEngine() {
closeSoftKeyboard()
addSearchEngineSaveButton().click()
}
fun typeCustomEngineDetails(engineName: String, engineURL: String) {
mDevice.findObject(By.res("$packageName:id/edit_engine_name")).clear()
mDevice.findObject(By.res("$packageName:id/edit_engine_name")).setText(engineName)
mDevice.findObject(By.res("$packageName:id/edit_search_string")).clear()
mDevice.findObject(By.res("$packageName:id/edit_search_string")).setText(engineURL)
try {
mDevice.findObject(By.res("$packageName:id/edit_engine_name")).clear()
mDevice.findObject(By.res("$packageName:id/edit_engine_name")).text = engineName
assertTrue(
mDevice.findObject(
UiSelector()
@ -213,6 +272,8 @@ class SettingsSubMenuSearchRobot {
).waitForExists(waitingTime),
)
mDevice.findObject(By.res("$packageName:id/edit_search_string")).clear()
mDevice.findObject(By.res("$packageName:id/edit_search_string")).text = engineURL
assertTrue(
mDevice.findObject(
UiSelector()
@ -223,16 +284,8 @@ class SettingsSubMenuSearchRobot {
} catch (e: AssertionError) {
println("The name or the search string were not set properly")
// Lets again set both name and search string
goBackButton().click()
openAddSearchEngineMenu()
selectAddCustomSearchEngine()
mDevice.findObject(By.res("$packageName:id/edit_engine_name")).clear()
mDevice.findObject(By.res("$packageName:id/edit_engine_name")).setText(engineName)
mDevice.findObject(By.res("$packageName:id/edit_search_string")).clear()
mDevice.findObject(By.res("$packageName:id/edit_search_string")).setText(engineURL)
assertTrue(
mDevice.findObject(
UiSelector()
@ -241,6 +294,8 @@ class SettingsSubMenuSearchRobot {
).waitForExists(waitingTime),
)
mDevice.findObject(By.res("$packageName:id/edit_search_string")).clear()
mDevice.findObject(By.res("$packageName:id/edit_search_string")).setText(engineURL)
assertTrue(
mDevice.findObject(
UiSelector()
@ -251,27 +306,36 @@ class SettingsSubMenuSearchRobot {
}
}
fun typeSearchEngineSuggestionString(searchSuggestionString: String) {
onView(withId(R.id.edit_suggest_string))
.click()
.perform(clearText())
.perform(typeText(searchSuggestionString))
}
// Used in the non-Compose Default search engines menu
fun openEngineOverflowMenu(searchEngineName: String) {
mDevice.findObject(
UiSelector().resourceId("org.mozilla.fenix.debug:id/overflow_menu"),
).waitForExists(waitingTime)
threeDotMenu(searchEngineName).waitForExists(waitingTimeShort)
threeDotMenu(searchEngineName).click()
}
fun deleteMultipleSearchEngines(vararg searchEngines: String) {
for (searchEngine in searchEngines) {
openEngineOverflowMenu(searchEngine)
clickDeleteSearchEngine()
}
// Used in the composable Manage shortcuts menu, otherwise the overflow menu is not visible
fun openCustomShortcutOverflowMenu(testRule: ComposeTestRule, searchEngineName: String) {
testRule.onNode(overflowMenuWithSiblingText(searchEngineName)).performClick()
}
fun clickEdit() = onView(withText("Edit")).click()
// Used in the Default search engine menu
fun clickDeleteSearchEngine() =
mDevice.findObject(
UiSelector().textContains(getStringResource(R.string.search_engine_delete)),
).click()
// Used in the composable Manage search shortcuts menu, otherwise the overflow menu is not visible
fun clickDeleteSearchEngine(testRule: ComposeTestRule) =
testRule.onNodeWithText("Delete").performClick()
fun clickUndoSnackBarButton() =
mDevice.findObject(
UiSelector()
@ -287,7 +351,17 @@ class SettingsSubMenuSearchRobot {
)
}
fun verifyShowSearchEnginesToggleState(enabled: Boolean) = assertShowSearchEnginesToggle(enabled)
fun verifyInvalidTemplateSearchStringFormatError() {
closeSoftKeyboard()
onView(withText(getStringResource(R.string.search_add_custom_engine_error_missing_template)))
.check(matches(isDisplayed()))
}
fun verifyErrorConnectingToSearchString(searchEngineName: String) {
closeSoftKeyboard()
onView(withText(getStringResource(R.string.search_add_custom_engine_error_cannot_reach, searchEngineName)))
.check(matches(isDisplayed()))
}
class Transition {
fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
@ -297,125 +371,131 @@ class SettingsSubMenuSearchRobot {
SettingsRobot().interact()
return SettingsRobot.Transition()
}
fun clickCustomSearchStringLearnMoreLink(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
onView(withId(R.id.custom_search_engines_learn_more)).click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun clickCustomSearchSuggestionsLearnMoreLink(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
onView(withId(R.id.custom_search_suggestions_learn_more)).click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
}
}
fun searchSettingsScreen(interact: SettingsSubMenuSearchRobot.() -> Unit): SettingsSubMenuSearchRobot.Transition {
SettingsSubMenuSearchRobot().interact()
return SettingsSubMenuSearchRobot.Transition()
}
/**
* Matches search shortcut items inside the 'Manage search shortcuts' menu
* @param name, of type String, should be the name of the search engine.
* @param checkboxIndex, of type Int, is the checkbox' index afferent to the search engine.
* @param isChecked, of type Boolean, should show if the checkbox is expected to be checked.
*/
class EngineShortcut(
val name: String,
val checkboxIndex: Int,
val isChecked: Boolean = true,
)
private fun assertSearchToolbar() =
onView(
allOf(
withId(R.id.navigationToolbar),
hasDescendant(withContentDescription(R.string.action_bar_up_description)),
hasDescendant(withText(R.string.preferences_search)),
private val defaultSearchEngineHeader = onView(withText("Default search engine"))
private val manageSearchShortcutsHeader = onView(withText("Manage search shortcuts"))
private fun searchHistorySwitchButton(): ViewInteraction {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText("Search browsing history")),
),
).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun assertDefaultSearchEngineHeader() =
onView(withText("Default search engine"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun assertSearchEngineList() {
onView(withText("Google"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
onView(withText("Amazon.com"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
onView(withText("Bing"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
onView(withText("DuckDuckGo"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
onView(withText("Wikipedia"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
onView(withText("Add search engine"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
)
return onView(withText("Search browsing history"))
}
private fun assertShowSearchShortcuts() {
private fun searchBookmarksSwitchButton(): ViewInteraction {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText("Show search engines")),
hasDescendant(withText("Search bookmarks")),
),
)
onView(withText("Show search engines"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
return onView(withText("Search bookmarks"))
}
private val searchHistoryToggle = onView(withText("Search browsing history"))
private val searchBookmarksToggle = onView(withText("Search bookmarks"))
private fun selectSearchEngine(searchEngine: String) {
onView(withText(searchEngine))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.perform(click())
private fun searchSyncedTabsSwitchButton(): ViewInteraction {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText("Search synced tabs")),
),
)
return onView(withText("Search synced tabs"))
}
private fun goBackButton() =
onView(CoreMatchers.allOf(withContentDescription("Navigate up")))
private fun voiceSearchSwitchButton(): ViewInteraction {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText("Show voice search")),
),
)
return onView(withText("Show voice search"))
}
private fun addSearchEngineButton() = onView(withText("Add search engine"))
private fun autocompleteSwitchButton(): ViewInteraction {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText(getStringResource(R.string.preferences_enable_autocomplete_urls))),
),
)
private fun assertAddSearchEngineList() {
onView(withText("Reddit")).check(matches(isDisplayed()))
onView(withText("YouTube")).check(matches(isDisplayed()))
onView(withText("Other")).check(matches(isDisplayed()))
return onView(withText(getStringResource(R.string.preferences_enable_autocomplete_urls)))
}
private fun addSearchEngineSaveButton() = onView(withId(R.id.add_search_engine))
private fun showSearchSuggestionSwitchButton(): ViewInteraction {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText("Show search suggestions")),
),
)
private fun assertEngineListContains(searchEngineName: String) {
onView(withId(R.id.search_engine_group)).check(matches(hasDescendant(withText(searchEngineName))))
return onView(withText("Show search suggestions"))
}
private fun assertDefaultSearchEngine(searchEngineName: String) =
onView(
allOf(
withId(R.id.radio_button),
withParent(withChild(withText(searchEngineName))),
private fun showClipboardSuggestionSwitch(): ViewInteraction {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText(getStringResource(R.string.preferences_show_clipboard_suggestions))),
),
).check(matches(isChecked(true)))
)
return onView(withText(getStringResource(R.string.preferences_show_clipboard_suggestions)))
}
private fun assertEngineListDoesNotContain(searchEngineName: String) {
onView(withId(R.id.search_engine_group)).check(matches(not(hasDescendant(withText(searchEngineName)))))
private fun showSuggestionsInPrivateModeSwitch(): ViewInteraction {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText(getStringResource(R.string.preferences_show_search_suggestions_in_private))),
),
)
return onView(withText(getStringResource(R.string.preferences_show_search_suggestions_in_private)))
}
private fun assertThreeDotButtonIsNotDisplayed(searchEngineName: String) =
threeDotMenu(searchEngineName).check(matches(not(isDisplayed())))
private fun goBackButton() =
onView(CoreMatchers.allOf(withContentDescription("Navigate up")))
private fun assertShowSearchEnginesToggle(enabled: Boolean) =
if (enabled) {
onView(withText(R.string.preferences_show_search_engines))
.check(
matches(
hasCousin(
allOf(
withClassName(endsWith("Switch")),
ViewMatchers.isChecked(),
),
),
),
)
} else {
onView(withText(R.string.preferences_show_search_engines))
.check(
matches(
hasCousin(
allOf(
withClassName(endsWith("Switch")),
ViewMatchers.isNotChecked(),
),
),
),
)
}
private val addSearchEngineButton = mDevice.findObject(UiSelector().text("Add search engine"))
private fun addSearchEngineSaveButton() = onView(withId(R.id.save_button))
private fun threeDotMenu(searchEngineName: String) =
mDevice.findObject(UiSelector().text(searchEngineName))
.getFromParent(UiSelector().description("More options"))
private fun defaultSearchEngineOption(searchEngineName: String) =
onView(
allOf(
withId(R.id.overflow_menu),
withParent(withChild(withText(searchEngineName))),
withId(R.id.radio_button),
hasSibling(withText(searchEngineName)),
),
)
private fun overflowMenuWithSiblingText(text: String): SemanticsMatcher =
hasAnySibling(hasText(text)) and hasContentDescription("More options")

@ -25,7 +25,16 @@ import org.mozilla.fenix.helpers.click
class SyncSignInRobot {
fun verifyAccountSettingsMenuHeader() = assertAccountSettingsMenuHeader()
fun verifyTurnOnSyncMenu() = assertTurnOnSyncMenu()
fun verifyTurnOnSyncMenu() {
mDevice.findObject(UiSelector().resourceId("$packageName:id/container")).waitForExists(waitingTime)
assertTrue(
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/signInScanButton")
.resourceId("$packageName:id/signInEmailButton"),
).waitForExists(waitingTime),
)
}
class Transition {
fun goBack(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
@ -46,14 +55,3 @@ private fun assertAccountSettingsMenuHeader() {
onView(withText(R.string.preferences_account_settings))
.check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
}
private fun assertTurnOnSyncMenu() {
mDevice.findObject(UiSelector().resourceId("$packageName:id/container")).waitForExists(waitingTime)
assertTrue(
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/signInScanButton")
.resourceId("$packageName:id/signInEmailButton"),
).waitForExists(waitingTime),
)
}

@ -39,6 +39,8 @@ import org.hamcrest.Matcher
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION
import org.mozilla.fenix.helpers.Constants.RETRY_COUNT
import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdAndDescriptionExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdAndTextExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdExists
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
@ -48,6 +50,7 @@ import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeLong
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
import org.mozilla.fenix.helpers.TestHelper.getStringResource
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
@ -81,7 +84,9 @@ class TabDrawerRobot {
assertPrivateBrowsingButtonIsSelected(isSelected)
fun verifySyncedTabsButtonIsSelected(isSelected: Boolean) =
assertSyncedTabsButtonIsSelected(isSelected)
syncedTabsButton().check(matches(isSelected(isSelected)))
fun clickSyncedTabsButton() = syncedTabsButton().click()
fun verifyExistingOpenTabs(vararg titles: String) = assertExistingOpenTabs(*titles)
fun verifyNoExistingOpenTabs(vararg titles: String) = assertNoExistingOpenTabs(*titles)
@ -272,6 +277,20 @@ class TabDrawerRobot {
itemContainingText("$numOfTabs selected"),
)
fun verifySyncedTabsListWhenUserIsNotSignedIn() {
assertItemWithResIdExists(itemWithResId("$packageName:id/tabsTray"))
assertItemContainingTextExists(
itemContainingText(getStringResource(R.string.synced_tabs_sign_in_message)),
itemContainingText(getStringResource(R.string.sync_sign_in)),
)
assertItemWithResIdAndDescriptionExists(
itemWithResIdAndDescription(
"$packageName:id/new_tab_button",
getStringResource(R.string.resync_button_content_description),
),
)
}
class Transition {
fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): Transition {
mDevice.waitForIdle(waitingTime)
@ -315,6 +334,19 @@ class TabDrawerRobot {
return Transition()
}
fun toggleToSyncedTabs(interact: TabDrawerRobot.() -> Unit): Transition {
syncedTabsButton().perform(click())
TabDrawerRobot().interact()
return Transition()
}
fun clickSignInToSyncButton(interact: SyncSignInRobot.() -> Unit): Transition {
itemContainingText(getStringResource(R.string.sync_sign_in))
.clickAndWaitForNewWindow(waitingTimeShort)
SyncSignInRobot().interact()
return Transition()
}
fun openTabsListThreeDotMenu(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition {
threeDotMenu().perform(click())
@ -593,10 +625,6 @@ private fun assertPrivateBrowsingButtonIsSelected(isSelected: Boolean) {
privateBrowsingButton().check(matches(isSelected(isSelected)))
}
private fun assertSyncedTabsButtonIsSelected(isSelected: Boolean) {
syncedTabsButton().check(matches(isSelected(isSelected)))
}
private val tabsList =
UiScrollable(UiSelector().className("androidx.recyclerview.widget.RecyclerView"))

@ -8,6 +8,7 @@ package org.mozilla.fenix.ui.robots
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.mozilla.fenix.helpers.HomeActivityComposeTestRule
import org.mozilla.fenix.helpers.click
/**
@ -45,6 +46,13 @@ class ThreeDotMenuBookmarksRobot {
return TabDrawerRobot.Transition()
}
fun clickOpenInNewTab(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
openInNewTabButton().click()
ComposeTabDrawerRobot(composeTestRule).interact()
return ComposeTabDrawerRobot.Transition(composeTestRule)
}
fun clickOpenInPrivateTab(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
openInPrivateTabButton().click()
@ -52,6 +60,13 @@ class ThreeDotMenuBookmarksRobot {
return TabDrawerRobot.Transition()
}
fun clickOpenInPrivateTab(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
openInPrivateTabButton().click()
ComposeTabDrawerRobot(composeTestRule).interact()
return ComposeTabDrawerRobot.Transition(composeTestRule)
}
fun clickOpenAllInTabs(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
openAllInTabsButton().click()
@ -59,6 +74,13 @@ class ThreeDotMenuBookmarksRobot {
return TabDrawerRobot.Transition()
}
fun clickOpenAllInTabs(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
openAllInTabsButton().click()
ComposeTabDrawerRobot(composeTestRule).interact()
return ComposeTabDrawerRobot.Transition(composeTestRule)
}
fun clickOpenAllInPrivateTabs(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
openAllInPrivateTabsButton().click()
@ -66,6 +88,13 @@ class ThreeDotMenuBookmarksRobot {
return TabDrawerRobot.Transition()
}
fun clickOpenAllInPrivateTabs(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
openAllInPrivateTabsButton().click()
ComposeTabDrawerRobot(composeTestRule).interact()
return ComposeTabDrawerRobot.Transition(composeTestRule)
}
fun clickDelete(interact: BookmarksRobot.() -> Unit): BookmarksRobot.Transition {
deleteButton().click()

@ -47,6 +47,7 @@ import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.nimbus.FxNimbus
/**
* Implementation of Robot Pattern for the three dot (main) menu.
@ -78,7 +79,8 @@ class ThreeDotMenuMainRobot {
fun verifySelectTabs() = assertSelectTabsButton()
fun verifyFindInPageButton() = assertItemContainingTextExists(findInPageButton)
fun verifyAddToShortcutsButton() = assertItemContainingTextExists(addToShortcutsButton)
fun verifyAddToShortcutsButton(shouldExist: Boolean) =
assertItemContainingTextExists(addToShortcutsButton, exists = shouldExist)
fun verifyRemoveFromShortcutsButton() = assertRemoveFromShortcutsButton()
fun verifyShareTabsOverlay() = assertShareTabsOverlay()
@ -110,7 +112,7 @@ class ThreeDotMenuMainRobot {
assertItemContainingTextExists(
settingsButton(),
)
if (FeatureFlags.print) {
if (FeatureFlags.print && FxNimbus.features.print.value().browserPrintEnabled) {
assertItemContainingTextExists(printContentButton)
}
assertItemWithDescriptionExists(

@ -50,7 +50,7 @@ object FeatureFlags {
/**
* Enables compose on the tabs tray items.
*/
const val composeTabsTray = false
val composeTabsTray = Config.channel.isNightlyOrDebug
/**
* Enables compose on the top sites.
@ -67,4 +67,9 @@ object FeatureFlags {
* Enables printing from the share and primary menu.
*/
val print = Config.channel.isNightlyOrDebug
/**
* Enables the lib-state HistoryFragment refactor
*/
val historyFragmentLibStateRefactor = Config.channel.isNightlyOrDebug
}

@ -49,6 +49,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.MediaSessionAction
import mozilla.components.browser.state.action.SearchAction
import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
@ -82,6 +83,7 @@ import mozilla.components.support.utils.toSafeIntent
import mozilla.components.support.webextensions.WebExtensionPopupFeature
import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.experiments.nimbus.initializeTooling
import org.mozilla.fenix.GleanMetrics.AppIcon
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.Metrics
import org.mozilla.fenix.GleanMetrics.PlayStoreAttribution
@ -150,6 +152,7 @@ import org.mozilla.fenix.settings.search.SaveSearchEngineFragmentDirections
import org.mozilla.fenix.settings.studies.StudiesFragmentDirections
import org.mozilla.fenix.settings.wallpaper.WallpaperSettingsFragmentDirections
import org.mozilla.fenix.share.AddNewDeviceFragmentDirections
import org.mozilla.fenix.shortcut.NewTabShortcutIntentProcessor.Companion.ACTION_OPEN_PRIVATE_TAB
import org.mozilla.fenix.tabhistory.TabHistoryDialogFragment
import org.mozilla.fenix.tabstray.TabsTrayFragment
import org.mozilla.fenix.tabstray.TabsTrayFragmentDirections
@ -329,6 +332,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
Events.appOpened.record(Events.AppOpenedExtra(it))
// This will record an event in Nimbus' internal event store. Used for behavioral targeting
components.analytics.experiments.recordEvent("app_opened")
if (safeIntent.action.equals(ACTION_OPEN_PRIVATE_TAB) && it == APP_ICON) {
AppIcon.newPrivateTabTapped.record(NoExtras())
}
}
}
supportActionBar?.hide()
@ -606,6 +613,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.core.pocketStoriesService.stopPeriodicSponsoredStoriesRefresh()
privateNotificationObserver?.stop()
components.notificationsDelegate.unBindActivity(this)
stopMediaSession()
}
override fun onConfigurationChanged(newConfig: Configuration) {
@ -834,7 +842,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
@VisibleForTesting(otherwise = PROTECTED)
internal open fun getIntentSource(intent: SafeIntent): String? {
return when {
intent.isLauncherIntent -> "APP_ICON"
intent.isLauncherIntent -> APP_ICON
intent.action == Intent.ACTION_VIEW -> "LINK"
else -> null
}
@ -876,6 +884,23 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
themeManager.applyStatusBarTheme(this)
}
// Stop active media when activity is destroyed.
private fun stopMediaSession() {
if (isFinishing) {
components.core.store.state.tabs.forEach {
it.mediaSessionState?.controller?.stop()
}
components.core.store.state.findActiveMediaTab()?.let {
components.core.store.dispatch(
MediaSessionAction.DeactivatedMediaSessionAction(
it.id,
),
)
}
}
}
/**
* Returns the [supportActionBar], inflating it if necessary.
* Everyone should call this instead of supportActionBar.
@ -1265,6 +1290,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
const val PRIVATE_BROWSING_MODE = "private_browsing_mode"
const val START_IN_RECENTS_SCREEN = "start_in_recents_screen"
const val OPEN_PASSWORD_MANAGER = "open_password_manager"
const val APP_ICON = "APP_ICON"
// PWA must have been used within last 30 days to be considered "recently used" for the
// telemetry purposes.

@ -141,6 +141,7 @@ import org.mozilla.fenix.ext.getFenixAddons
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.navigateWithBreadcrumb
import org.mozilla.fenix.ext.registerForActivityResult
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached
@ -1224,6 +1225,13 @@ abstract class BaseBrowserFragment :
components.useCases.sessionUseCases.reload()
}
hideToolbar()
context?.settings()?.shouldOpenLinksInApp(customTabSessionId != null)
?.let { openLinksInExternalApp ->
components.services.appLinksInterceptor.updateLaunchInApp {
openLinksInExternalApp
}
}
}
@CallSuper
@ -1431,12 +1439,14 @@ abstract class BaseBrowserFragment :
MetricsUtils.BookmarkAction.EDIT,
TOAST_METRIC_SOURCE,
)
nav(
R.id.browserFragment,
BrowserFragmentDirections.actionGlobalBookmarkEditFragment(
findNavController().navigateWithBreadcrumb(
directions = BrowserFragmentDirections.actionGlobalBookmarkEditFragment(
guid,
true,
),
navigateFrom = "BrowserFragment",
navigateTo = "ActionGlobalBookmarkEditFragment",
crashReporter = it.context.components.analytics.crashReporter,
)
}
.show()

@ -62,6 +62,8 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
private val windowFeature = ViewBoundFeatureWrapper<WindowFeature>()
private val openInAppOnboardingObserver = ViewBoundFeatureWrapper<OpenInAppOnboardingObserver>()
private val standardSnackbarErrorBinding =
ViewBoundFeatureWrapper<StandardSnackbarErrorBinding>()
private val reviewQualityCheckFeature = ViewBoundFeatureWrapper<ReviewQualityCheckFeature>()
private var readerModeAvailable = false
@ -96,7 +98,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
val homeAction = BrowserToolbar.Button(
imageDrawable = AppCompatResources.getDrawable(
context,
R.drawable.mozac_ic_home,
R.drawable.mozac_ic_home_24,
)!!,
contentDescription = context.getString(R.string.browser_toolbar_home),
iconTintColorResource = ThemeManager.resolveAttribute(R.attr.textPrimary, context),
@ -188,6 +190,14 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
if (!context.settings().shouldUseCookieBanner && !context.settings().userOptOutOfReEngageCookieBannerDialog) {
observeCookieBannerHandlingState(context.components.core.store)
}
standardSnackbarErrorBinding.set(
feature = StandardSnackbarErrorBinding(
requireActivity(),
requireActivity().components.appStore,
),
owner = viewLifecycleOwner,
view = binding.root,
)
}
private fun initReviewQualityCheck(context: Context, view: View) {
@ -250,7 +260,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
backAction = BrowserToolbar.TwoStateButton(
primaryImage = AppCompatResources.getDrawable(
context,
R.drawable.mozac_ic_back,
R.drawable.mozac_ic_back_24,
)!!,
primaryContentDescription = context.getString(R.string.browser_menu_back),
primaryImageTintResource = enableTint,
@ -278,7 +288,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
forwardAction = BrowserToolbar.TwoStateButton(
primaryImage = AppCompatResources.getDrawable(
context,
R.drawable.mozac_ic_forward,
R.drawable.mozac_ic_forward_24,
)!!,
primaryContentDescription = context.getString(R.string.browser_menu_forward),
primaryImageTintResource = enableTint,
@ -306,7 +316,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
refreshAction = BrowserToolbar.TwoStateButton(
primaryImage = AppCompatResources.getDrawable(
context,
R.drawable.mozac_ic_refresh,
R.drawable.mozac_ic_arrow_clockwise_24,
)!!,
primaryContentDescription = context.getString(R.string.browser_menu_refresh),
primaryImageTintResource = enableTint,

@ -0,0 +1,89 @@
/* 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.browser
import android.app.Activity
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import mozilla.components.lib.state.helpers.AbstractBinding
import org.mozilla.fenix.R
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.components.appstate.AppState
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getRootView
/**
* A binding that shows standard snackbar error.
*/
@OptIn(ExperimentalCoroutinesApi::class)
class StandardSnackbarErrorBinding(
private val activity: Activity,
appStore: AppStore,
) : AbstractBinding<AppState>(appStore) {
override suspend fun onState(flow: Flow<AppState>) {
flow.map { state -> state.standardSnackbarError }
.distinctUntilChanged()
.collect {
it?.let { standardSnackbarError ->
activity.getRootView()?.let { view ->
val snackBar = FenixSnackbar.make(
view = view,
duration = Snackbar.LENGTH_INDEFINITE,
isDisplayedWithBrowserToolbar = true,
)
snackBar.setText(
standardSnackbarError.message,
)
snackBar.setButtonTextColor(
ContextCompat.getColor(
activity,
R.color.fx_mobile_text_color_primary,
),
)
snackBar.setBackground(
AppCompatResources.getDrawable(
activity,
R.drawable.standard_snackbar_error_background,
),
)
snackBar.setSnackBarTextColor(
ContextCompat.getColor(
activity,
R.color.fx_mobile_text_color_warning,
),
)
snackBar.setAction(
text = activity.getString(R.string.standard_snackbar_error_dismiss),
action = {
view.context.components.appStore.dispatch(
AppAction.UpdateStandardSnackbarErrorAction(
null,
),
)
},
)
snackBar.show()
}
}
}
}
}
/**
* Standard Snackbar Error data class.
*
* @param message that will appear on the snackbar.
*/
data class StandardSnackbarError(
val message: String,
)

@ -248,6 +248,16 @@ class Core(
* The [BrowserStore] holds the global [BrowserState].
*/
val store by lazyMonitored {
val searchExtraParamsNimbus = FxNimbus.features.searchExtraParams.value()
val searchExtraParams = searchExtraParamsNimbus.takeIf { it.enabled }?.run {
SearchExtraParams(
searchEngine,
featureEnabler.keys.firstOrNull(),
featureEnabler.values.firstOrNull(),
channelId.keys.first(),
channelId.values.first(),
)
}
val middlewareList =
mutableListOf(
LastAccessMiddleware(),
@ -262,14 +272,7 @@ class Core(
context = context,
additionalBundledSearchEngineIds = listOf("reddit", "youtube"),
migration = SearchMigration(context),
searchExtraParams =
FxNimbus.features.searchExtraParams.value().searchNameChannelId
.firstNotNullOfOrNull {
SearchExtraParams(
it.key,
it.value,
)
},
searchExtraParams = searchExtraParams,
),
RecordingDevicesMiddleware(context, context.components.notificationsDelegate),
PromptMiddleware(),

@ -5,6 +5,7 @@
package org.mozilla.fenix.components
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
@ -54,6 +55,27 @@ class FenixSnackbar private constructor(
}
}
/**
* Set FenixSnackBar background.
*/
fun setBackground(backgroundDrawable: Drawable?) = this.apply {
binding.snackbarLayout.background = backgroundDrawable
}
/**
* Set FenixSnackBar button text color.
*/
fun setButtonTextColor(textColor: Int) = this.apply {
binding.snackbarBtn.setTextColor(textColor)
}
/**
* Set FenixSnackBarText text color.
*/
fun setSnackBarTextColor(textColor: Int) = this.apply {
binding.snackbarText.setTextColor(textColor)
}
fun setText(text: String) = this.apply {
binding.snackbarText.text = text
}
@ -62,6 +84,11 @@ class FenixSnackbar private constructor(
this.duration = duration
}
/**
* Set snackBar button action
* @param text that should appear on snackBar button.
* @param action callback when user presses snackBar button.
*/
fun setAction(text: String, action: () -> Unit) = this.apply {
binding.snackbarBtn.apply {
setText(text)

@ -12,6 +12,7 @@ import mozilla.components.service.nimbus.messaging.Message
import mozilla.components.service.nimbus.messaging.MessageSurfaceId
import mozilla.components.service.pocket.PocketStory
import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
import org.mozilla.fenix.browser.StandardSnackbarError
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.home.Mode
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
@ -208,4 +209,11 @@ sealed class AppAction : Action {
*/
object PauseAction : AppLifecycleAction()
}
/**
* State of standard error snackBar has changed.
*/
data class UpdateStandardSnackbarErrorAction(
val standardSnackbarError: StandardSnackbarError?,
) : AppAction()
}

@ -12,6 +12,7 @@ import mozilla.components.lib.state.State
import mozilla.components.service.pocket.PocketStory
import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
import org.mozilla.fenix.browser.StandardSnackbarError
import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.home.Mode
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
@ -71,4 +72,5 @@ data class AppState(
val messaging: MessagingState = MessagingState(),
val pendingDeletionHistoryItems: Set<PendingDeletionHistory> = emptySet(),
val wallpaperState: WallpaperState = WallpaperState.default,
val standardSnackbarError: StandardSnackbarError? = null,
) : State

@ -227,6 +227,10 @@ internal object AppStoreReducer {
is AppAction.AppLifecycleAction.PauseAction -> {
state.copy(isForeground = false)
}
is AppAction.UpdateStandardSnackbarErrorAction -> state.copy(
standardSnackbarError = action.standardSnackbarError,
)
}
}

@ -20,12 +20,14 @@ import mozilla.components.feature.media.facts.MediaFacts
import mozilla.components.feature.prompts.dialog.LoginDialogFacts
import mozilla.components.feature.prompts.facts.AddressAutofillDialogFacts
import mozilla.components.feature.prompts.facts.CreditCardAutofillDialogFacts
import mozilla.components.feature.prompts.facts.LoginAutofillDialogFacts
import mozilla.components.feature.pwa.ProgressiveWebAppFacts
import mozilla.components.feature.search.telemetry.ads.AdsTelemetry
import mozilla.components.feature.search.telemetry.incontent.InContentTelemetry
import mozilla.components.feature.sitepermissions.SitePermissionsFacts
import mozilla.components.feature.syncedtabs.facts.SyncedTabsFacts
import mozilla.components.feature.top.sites.facts.TopSitesFacts
import mozilla.components.service.fxa.SyncFacts
import mozilla.components.support.base.Component
import mozilla.components.support.base.facts.Action
import mozilla.components.support.base.facts.Fact
@ -45,11 +47,13 @@ import org.mozilla.fenix.GleanMetrics.ContextualMenu
import org.mozilla.fenix.GleanMetrics.CreditCards
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.LoginDialog
import org.mozilla.fenix.GleanMetrics.Logins
import org.mozilla.fenix.GleanMetrics.MediaNotification
import org.mozilla.fenix.GleanMetrics.MediaState
import org.mozilla.fenix.GleanMetrics.PerfAwesomebar
import org.mozilla.fenix.GleanMetrics.ProgressiveWebApp
import org.mozilla.fenix.GleanMetrics.SitePermissions
import org.mozilla.fenix.GleanMetrics.Sync
import org.mozilla.fenix.GleanMetrics.SyncedTabs
import org.mozilla.fenix.search.awesomebar.ShortcutsSuggestionProvider
import org.mozilla.fenix.utils.Settings
@ -195,6 +199,13 @@ internal class ReleaseMetricController(
Component.FEATURE_PROMPTS to AddressAutofillDialogFacts.Items.AUTOFILL_ADDRESS_PROMPT_DISMISSED ->
Addresses.autofillPromptDismissed.record(NoExtras())
Component.FEATURE_PROMPTS to LoginAutofillDialogFacts.Items.AUTOFILL_LOGIN_PERFORMED ->
Logins.autofilled.record(NoExtras())
Component.FEATURE_PROMPTS to LoginAutofillDialogFacts.Items.AUTOFILL_LOGIN_PROMPT_SHOWN ->
Logins.autofillPromptShown.record(NoExtras())
Component.FEATURE_PROMPTS to LoginAutofillDialogFacts.Items.AUTOFILL_LOGIN_PROMPT_DISMISSED ->
Logins.autofillPromptDismissed.record(NoExtras())
Component.FEATURE_AUTOFILL to AutofillFacts.Items.AUTOFILL_REQUEST -> {
val hasMatchingLogins =
metadata?.get(AutofillFacts.Metadata.HAS_MATCHING_LOGINS) as Boolean?
@ -352,6 +363,9 @@ internal class ReleaseMetricController(
}
}
}
Component.SERVICE_FIREFOX_ACCOUNTS to SyncFacts.Items.SYNC_FAILED -> {
Sync.failed.record(NoExtras())
}
else -> {
// no-op

@ -40,6 +40,7 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.components.accounts.FenixAccountManager
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.theme.ThemeManager
/**
@ -71,12 +72,14 @@ open class DefaultToolbarMenu(
private val shouldDeleteDataOnQuit = context.settings().shouldDeleteBrowsingDataOnQuit
private val shouldUseBottomToolbar = context.settings().shouldUseBottomToolbar
private val shouldShowTopSites = context.settings().showTopSitesFeature
private val accountManager = FenixAccountManager(context)
private val selectedSession: TabSessionState?
get() = store.state.selectedTab
override val menuBuilder by lazy {
FxNimbus.features.print.recordExposure()
WebExtensionBrowserMenuBuilder(
items = coreMenuItems,
endOfMenuAlwaysVisible = shouldUseBottomToolbar,
@ -94,7 +97,7 @@ open class DefaultToolbarMenu(
override val menuToolbar by lazy {
val back = BrowserMenuItemToolbar.TwoStateButton(
primaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_back,
primaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_back_24,
primaryContentDescription = context.getString(R.string.browser_menu_back),
primaryImageTintResource = primaryTextColor(),
isInPrimaryState = {
@ -108,7 +111,7 @@ open class DefaultToolbarMenu(
}
val forward = BrowserMenuItemToolbar.TwoStateButton(
primaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_forward,
primaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_forward_24,
primaryContentDescription = context.getString(R.string.browser_menu_forward),
primaryImageTintResource = primaryTextColor(),
isInPrimaryState = {
@ -122,7 +125,7 @@ open class DefaultToolbarMenu(
}
val refresh = BrowserMenuItemToolbar.TwoStateButton(
primaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_refresh,
primaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_arrow_clockwise_24,
primaryContentDescription = context.getString(R.string.browser_menu_refresh),
primaryImageTintResource = primaryTextColor(),
isInPrimaryState = {
@ -180,7 +183,7 @@ open class DefaultToolbarMenu(
private val installToHomescreen = BrowserMenuHighlightableItem(
label = context.getString(R.string.browser_menu_install_on_homescreen),
startImageResource = R.drawable.mozac_ic_add_to_home_screen,
startImageResource = R.drawable.mozac_ic_add_to_home_screen_24,
iconTintColorResource = primaryTextColor(),
highlight = BrowserMenuHighlight.LowPriority(
label = context.getString(R.string.browser_menu_install_on_homescreen),
@ -225,7 +228,7 @@ open class DefaultToolbarMenu(
private val findInPageItem = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_find_in_page),
imageResource = R.drawable.mozac_ic_search,
imageResource = R.drawable.mozac_ic_search_24,
iconTintColorResource = primaryTextColor(),
) {
onItemTapped.invoke(ToolbarMenu.Item.FindInPage)
@ -269,7 +272,7 @@ open class DefaultToolbarMenu(
private val addToHomeScreenItem = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_add_to_homescreen),
imageResource = R.drawable.mozac_ic_add_to_home_screen,
imageResource = R.drawable.mozac_ic_add_to_home_screen_24,
iconTintColorResource = primaryTextColor(),
isCollapsingMenuLimit = true,
) {
@ -313,7 +316,7 @@ open class DefaultToolbarMenu(
@VisibleForTesting
internal val settingsItem = BrowserMenuHighlightableItem(
label = context.getString(R.string.browser_menu_settings),
startImageResource = R.drawable.mozac_ic_settings,
startImageResource = R.drawable.mozac_ic_settings_24,
iconTintColorResource = if (hasAccountProblem) {
ThemeManager.resolveAttribute(R.attr.syncDisconnected, context)
} else {
@ -353,7 +356,7 @@ open class DefaultToolbarMenu(
private val deleteDataOnQuit = BrowserMenuImageText(
label = context.getString(R.string.delete_browsing_data_on_quit_action),
imageResource = R.drawable.mozac_ic_quit,
imageResource = R.drawable.mozac_ic_cross_circle_24,
iconTintColorResource = primaryTextColor(),
) {
onItemTapped.invoke(ToolbarMenu.Item.Quit)
@ -388,9 +391,9 @@ open class DefaultToolbarMenu(
BrowserMenuDivider(),
addToHomeScreenItem.apply { visible = ::canAddToHomescreen },
installToHomescreen.apply { visible = ::canInstall },
addRemoveTopSitesItem,
if (shouldShowTopSites) addRemoveTopSitesItem else null,
saveToCollectionItem,
if (FeatureFlags.print) printPageItem else null,
if (FeatureFlags.print && FxNimbus.features.print.value().browserPrintEnabled) printPageItem else null,
BrowserMenuDivider(),
settingsItem,
if (shouldDeleteDataOnQuit) deleteDataOnQuit else null,

@ -15,6 +15,7 @@ import androidx.compose.material.Card
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@ -37,11 +38,13 @@ import org.mozilla.fenix.theme.FirefoxTheme
* ```
*
* @param text The only [String] that this will display.
* @param backgroundColor The background [Color] of the list item placeholder.
* @param onClick Optional callback to be invoked when this composable is clicked.
*/
@Composable
fun ListItemTabLargePlaceholder(
text: String,
backgroundColor: Color,
onClick: () -> Unit = { },
) {
Card(
@ -49,7 +52,7 @@ fun ListItemTabLargePlaceholder(
.size(ITEM_WIDTH.dp, ITEM_HEIGHT.dp)
.clickable { onClick() },
shape = RoundedCornerShape(8.dp),
backgroundColor = FirefoxTheme.colors.layer2,
backgroundColor = backgroundColor,
elevation = 6.dp,
) {
Column(
@ -74,6 +77,9 @@ fun ListItemTabLargePlaceholder(
@Preview
private fun ListItemTabLargePlaceholderPreview() {
FirefoxTheme {
ListItemTabLargePlaceholder(text = "Item placeholder")
ListItemTabLargePlaceholder(
text = "Item placeholder",
backgroundColor = FirefoxTheme.colors.layer2,
)
}
}

@ -145,7 +145,7 @@ private fun MessageCardIconButton(
onClick = onCloseButtonClick,
) {
Icon(
painter = painterResource(R.drawable.mozac_ic_close_20),
painter = painterResource(R.drawable.mozac_ic_cross_20),
contentDescription = stringResource(
R.string.content_description_close_button,
),

@ -0,0 +1,181 @@
/* 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.compose
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.SwitchDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.unit.dp
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.theme.FirefoxTheme
import androidx.compose.material.Switch as MaterialSwitch
private const val DISABLED_ALPHA = 0.5f
/**
* UI for a switch with label that can be on or off.
*
* @param label Text to be displayed next to the switch.
* @param checked Whether or not the switch is checked.
* @param onCheckedChange Invoked when Switch is being clicked, therefore the change of checked
* state is requested.
* @param modifier Modifier to be applied to the switch layout.
* @param enabled Whether the switch is enabled or grayed out.
*/
@Composable
fun SwitchWithLabel(
label: String,
checked: Boolean,
onCheckedChange: ((Boolean) -> Unit),
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = label,
color = if (enabled) {
FirefoxTheme.colors.textPrimary
} else {
FirefoxTheme.colors.textDisabled
},
style = FirefoxTheme.typography.subtitle1,
modifier = Modifier.weight(1f),
)
Switch(
checked = checked,
onCheckedChange = onCheckedChange,
enabled = enabled,
)
}
}
/**
* UI for a switch that can be on or off.
*
* @param checked Whether or not the switch is checked.
* @param onCheckedChange Invoked when Switch is being clicked, therefore the change of checked
* state is requested.
* @param modifier Modifier to be applied to the switch layout.
* @param enabled Whether the switch is enabled or grayed out.
*/
@Composable
private fun Switch(
checked: Boolean,
onCheckedChange: ((Boolean) -> Unit),
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
MaterialSwitch(
checked = checked,
onCheckedChange = onCheckedChange,
modifier = modifier,
enabled = enabled,
colors = SwitchDefaults.colors(
uncheckedThumbColor = FirefoxTheme.colors.formOff,
uncheckedTrackColor = FirefoxTheme.colors.formSurface,
checkedThumbColor = FirefoxTheme.colors.formOn,
checkedTrackColor = FirefoxTheme.colors.formSurface,
disabledUncheckedThumbColor = FirefoxTheme.colors.formOff
.copy(alpha = DISABLED_ALPHA)
.compositeOver(FirefoxTheme.colors.formSurface),
disabledUncheckedTrackColor = FirefoxTheme.colors.formSurface.copy(alpha = DISABLED_ALPHA),
disabledCheckedThumbColor = FirefoxTheme.colors.formOn
.copy(alpha = DISABLED_ALPHA)
.compositeOver(FirefoxTheme.colors.formSurface),
disabledCheckedTrackColor = FirefoxTheme.colors.formSurface.copy(alpha = DISABLED_ALPHA),
),
)
}
@LightDarkPreview
@Composable
private fun SwitchWithLabelPreview() {
FirefoxTheme {
Column(
modifier = Modifier
.background(FirefoxTheme.colors.layer1)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
text = "Enabled",
style = FirefoxTheme.typography.headline7,
color = FirefoxTheme.colors.textPrimary,
)
Spacer(Modifier.height(8.dp))
var enabledSwitchState by remember { mutableStateOf(false) }
SwitchWithLabel(
label = if (enabledSwitchState) "On" else "Off",
checked = enabledSwitchState,
onCheckedChange = { enabledSwitchState = it },
)
Text(
text = "Disabled",
style = FirefoxTheme.typography.headline7,
color = FirefoxTheme.colors.textPrimary,
)
Spacer(Modifier.height(8.dp))
var disabledSwitchStateOff by remember { mutableStateOf(false) }
SwitchWithLabel(
label = "Off",
checked = disabledSwitchStateOff,
enabled = false,
onCheckedChange = { disabledSwitchStateOff = it },
)
var disabledSwitchStateOn by remember { mutableStateOf(true) }
SwitchWithLabel(
label = "On",
checked = disabledSwitchStateOn,
enabled = false,
onCheckedChange = { disabledSwitchStateOn = it },
)
Text(
text = "Nested",
style = FirefoxTheme.typography.headline7,
color = FirefoxTheme.colors.textPrimary,
)
Spacer(Modifier.height(8.dp))
Row {
Spacer(Modifier.weight(1f))
var nestedSwitchState by remember { mutableStateOf(false) }
SwitchWithLabel(
label = "Nested",
checked = nestedSwitchState,
onCheckedChange = { nestedSwitchState = it },
modifier = Modifier.weight(1f),
)
}
}
}
}

@ -9,7 +9,6 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.isSystemInDarkTheme
@ -32,6 +31,7 @@ import androidx.compose.material.Card
import androidx.compose.material.DismissValue
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.material.rememberDismissState
import androidx.compose.material.ripple.rememberRipple
@ -133,7 +133,11 @@ fun TabGridItem(
},
),
) {
Box(modifier = Modifier.wrapContentSize()) {
Box(
modifier = Modifier
.wrapContentSize()
.testTag(TabsTrayTestTag.tabItemRoot),
) {
Card(
modifier = Modifier
.fillMaxWidth()
@ -200,16 +204,21 @@ fun TabGridItem(
}
if (!multiSelectionEnabled) {
Icon(
painter = painterResource(id = R.drawable.mozac_ic_close),
contentDescription = stringResource(id = R.string.close_tab),
tint = FirefoxTheme.colors.iconPrimary,
IconButton(
modifier = Modifier
.clickable { onCloseClick(tab) }
.size(24.dp)
.align(Alignment.CenterVertically)
.testTag(TabsTrayTestTag.tabItemClose),
)
onClick = {
onCloseClick(tab)
},
) {
Icon(
painter = painterResource(id = R.drawable.mozac_ic_cross_20),
contentDescription = stringResource(id = R.string.close_tab),
tint = FirefoxTheme.colors.iconPrimary,
)
}
}
}
@ -275,7 +284,7 @@ private fun Thumbnail(
backgroundColor = FirefoxTheme.colors.layerAccent,
) {
Icon(
painter = painterResource(id = R.drawable.mozac_ic_check),
painter = painterResource(id = R.drawable.mozac_ic_checkmark_24),
modifier = Modifier
.matchParentSize()
.padding(all = 8.dp),

@ -124,7 +124,8 @@ fun TabListItem(
onLongClick = { onLongClick(tab) },
onClick = { onClick(tab) },
)
.padding(start = 16.dp, top = 8.dp, bottom = 8.dp),
.padding(start = 16.dp, top = 8.dp, bottom = 8.dp)
.testTag(TabsTrayTestTag.tabItemRoot),
verticalAlignment = Alignment.CenterVertically,
) {
Thumbnail(
@ -167,7 +168,7 @@ fun TabListItem(
.testTag(TabsTrayTestTag.tabItemClose),
) {
Icon(
painter = painterResource(id = R.drawable.mozac_ic_close),
painter = painterResource(id = R.drawable.mozac_ic_cross_24),
contentDescription = stringResource(
id = R.string.close_tab_title,
tab.content.title,
@ -217,7 +218,7 @@ private fun Thumbnail(
backgroundColor = FirefoxTheme.colors.layerAccent,
) {
Icon(
painter = painterResource(id = R.drawable.mozac_ic_check),
painter = painterResource(id = R.drawable.mozac_ic_checkmark_24),
modifier = Modifier
.matchParentSize()
.padding(all = 8.dp),

@ -55,7 +55,7 @@ class CustomTabToolbarMenu(
override val menuToolbar by lazy {
val back = BrowserMenuItemToolbar.TwoStateButton(
primaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_back,
primaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_back_24,
primaryContentDescription = context.getString(R.string.browser_menu_back),
primaryImageTintResource = primaryTextColor(),
isInPrimaryState = {
@ -72,7 +72,7 @@ class CustomTabToolbarMenu(
}
val forward = BrowserMenuItemToolbar.TwoStateButton(
primaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_forward,
primaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_forward_24,
primaryContentDescription = context.getString(R.string.browser_menu_forward),
primaryImageTintResource = primaryTextColor(),
isInPrimaryState = {
@ -89,7 +89,7 @@ class CustomTabToolbarMenu(
}
val refresh = BrowserMenuItemToolbar.TwoStateButton(
primaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_refresh,
primaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_arrow_clockwise_24,
primaryContentDescription = context.getString(R.string.browser_menu_refresh),
primaryImageTintResource = primaryTextColor(),
isInPrimaryState = {
@ -144,7 +144,7 @@ class CustomTabToolbarMenu(
private val findInPage = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_find_in_page),
imageResource = R.drawable.mozac_ic_search,
imageResource = R.drawable.mozac_ic_search_24,
iconTintColorResource = primaryTextColor(),
) {
onItemTapped.invoke(ToolbarMenu.Item.FindInPage)

@ -8,7 +8,6 @@ import android.content.Context
import mozilla.components.service.nimbus.NimbusApi
import mozilla.components.service.nimbus.NimbusAppInfo
import mozilla.components.service.nimbus.NimbusBuilder
import mozilla.components.service.nimbus.loggingErrorReporter
import mozilla.components.service.nimbus.messaging.FxNimbusMessaging
import mozilla.components.service.nimbus.messaging.NimbusSystem
import mozilla.components.support.base.log.logger.Logger
@ -30,6 +29,8 @@ import org.mozilla.fenix.utils.Settings
*/
private const val TIME_OUT_LOADING_EXPERIMENT_FROM_DISK_MS = 200L
private val logger = Logger("service/Nimbus")
/**
* Create the Nimbus singleton object for the Fenix app.
*/
@ -59,35 +60,32 @@ fun createNimbus(context: Context, urlString: String?): NimbusApi {
return NimbusBuilder(context).apply {
url = urlString
errorReporter = { message, e ->
if (BuildConfig.BUILD_TYPE == "debug") {
Logger.error("Nimbus error: $message", e)
}
if (e !is NimbusException || e.isReportableError()) {
@Suppress("TooGenericExceptionCaught")
try {
context.components.analytics.crashReporter.submitCaughtException(e)
} catch (e: Throwable) {
loggingErrorReporter(message, e)
}
}
}
errorReporter = context::reportError
initialExperiments = R.raw.initial_experiments
timeoutLoadingExperiment = TIME_OUT_LOADING_EXPERIMENT_FROM_DISK_MS
usePreviewCollection = context.settings().nimbusUsePreview
isFirstRun = isAppFirstRun
onCreateCallback = { nimbus ->
FxNimbus.initialize { nimbus }
}
onApplyCallback = {
FxNimbus.invalidateCachedValues()
}
onFetchedCallback = {
featureManifest = FxNimbus
onFetchCallback = {
context.settings().nimbusExperimentsFetched = true
}
}.build(appInfo)
}
private fun Context.reportError(message: String, e: Throwable) {
if (BuildConfig.BUILD_TYPE == "debug") {
logger.error("Nimbus error: $message", e)
}
if (e !is NimbusException || e.isReportableError()) {
@Suppress("TooGenericExceptionCaught")
try {
this.components.analytics.crashReporter.submitCaughtException(e)
} catch (e: Throwable) {
logger.error(message, e)
}
}
}
/**
* Classifies which errors we should forward to our crash reporter or not. We want to filter out the
* non-reportable ones if we know there is no reasonable action that we can perform.

@ -9,7 +9,10 @@ import androidx.annotation.IdRes
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.navigation.NavOptions
import mozilla.components.concept.base.crash.Breadcrumb
import mozilla.components.lib.crash.CrashReporter
import mozilla.components.support.base.log.logger.Logger
import java.lang.IllegalArgumentException
/**
* Navigate from the fragment with [id] using the given [directions].
@ -36,12 +39,35 @@ fun NavController.navigateSafe(
}
}
/**
* Navigates using the given [directions], and submit a Breadcrumb
* when an [IllegalArgumentException] happens.
*/
fun NavController.navigateWithBreadcrumb(
directions: NavDirections,
navigateFrom: String,
navigateTo: String,
crashReporter: CrashReporter,
) {
try {
this.navigate(directions)
} catch (e: IllegalArgumentException) {
crashReporter.recordCrashBreadcrumb(
Breadcrumb(
"Navigation - " +
"where we are: $currentDestination," + "where we are going: $navigateTo, " +
"where we thought we were: $navigateFrom",
),
)
}
}
/**
* Checks if the Fragment with a [fragmentClassName] is on top of the back queue.
*/
@SuppressLint("RestrictedApi")
fun NavController.hasTopDestination(fragmentClassName: String): Boolean {
return this.backQueue.lastOrNull()?.destination?.displayName?.contains(
return this.currentBackStackEntry?.destination?.displayName?.contains(
fragmentClassName,
true,
) == true

@ -56,6 +56,7 @@ object GeckoProvider {
.crashHandler(CrashHandlerService::class.java)
.telemetryDelegate(GeckoAdapter())
.contentBlocking(policy.toContentBlockingSetting())
.consoleOutput(context.components.settings.enableGeckoLogs)
.debugLogging(Config.channel.isDebug || context.components.settings.enableGeckoLogs)
.aboutConfigEnabled(true)
.build()

@ -62,6 +62,7 @@ import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.service.glean.private.NoExtras
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.mozilla.fenix.GleanMetrics.HomeScreen
import org.mozilla.fenix.GleanMetrics.Homepage
import org.mozilla.fenix.GleanMetrics.PrivateBrowsingShortcutCfr
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
@ -370,6 +371,8 @@ class HomeFragment : Fragment() {
activity = activity,
navController = findNavController(),
appStore = components.appStore,
browserStore = components.core.store,
selectTabUseCase = components.useCases.tabsUseCases.selectTab,
),
recentVisitsController = DefaultRecentVisitsController(
navController = findNavController(),
@ -555,6 +558,7 @@ class HomeFragment : Fragment() {
newMode,
userHasBeenOnboarded = true,
)
Homepage.privateModeIconTapped.record(mozilla.telemetry.glean.private.NoExtras())
}
consumeFrom(requireComponents.core.store) {

@ -24,7 +24,9 @@ import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.Config
import org.mozilla.fenix.GleanMetrics.AppMenu
import org.mozilla.fenix.R
import org.mozilla.fenix.components.accounts.AccountState
import org.mozilla.fenix.components.accounts.FenixAccountManager
@ -91,7 +93,7 @@ class HomeMenu(
private val quitItem by lazy {
BrowserMenuImageText(
context.getString(R.string.delete_browsing_data_on_quit_action),
R.drawable.mozac_ic_quit,
R.drawable.mozac_ic_cross_circle_24,
primaryTextColor,
) {
onItemTapped.invoke(Item.Quit)
@ -169,7 +171,7 @@ class HomeMenu(
val helpItem = BrowserMenuImageText(
context.getString(R.string.browser_menu_help),
R.drawable.mozac_ic_help,
R.drawable.mozac_ic_help_circle_24,
primaryTextColor,
) {
onItemTapped.invoke(Item.Help)
@ -181,13 +183,14 @@ class HomeMenu(
primaryTextColor,
) {
onItemTapped.invoke(Item.CustomizeHome)
AppMenu.customizeHomepage.record(NoExtras())
}
// Use nimbus to set the icon and title.
val nimbusValidation = FxNimbus.features.nimbusValidation.value()
val settingsItem = BrowserMenuImageText(
nimbusValidation.settingsTitle,
R.drawable.mozac_ic_settings,
R.drawable.mozac_ic_settings_24,
primaryTextColor,
) {
onItemTapped.invoke(Item.Settings)
@ -204,12 +207,18 @@ class HomeMenu(
null
}
// Since syncSignIn & accountAuth items take us to the same place -> we won't show them in the same time
// We will show syncSignIn item when the accountAuth item:
// 1. is not needed or
// 2. it is needed, but the account manager is not available yet
val syncSignInMenuItem = if (accountAuthItem == null) syncSignInMenuItem() else null
val menuItems = listOfNotNull(
bookmarksItem,
historyItem,
downloadsItem,
extensionsItem,
syncSignInMenuItem(),
syncSignInMenuItem,
accountAuthItem,
if (Config.channel.isMozillaOnline) manageAccountAndDevicesItem else null,
BrowserMenuDivider(),

@ -307,7 +307,10 @@ fun PocketStories(
},
) {
if (story == placeholderStory) {
ListItemTabLargePlaceholder(stringResource(R.string.pocket_stories_placeholder_text)) {
ListItemTabLargePlaceholder(
text = stringResource(R.string.pocket_stories_placeholder_text),
backgroundColor = backgroundColor,
) {
onDiscoverMoreClicked("https://getpocket.com/explore?$POCKET_FEATURE_UTM_KEY_VALUE")
}
} else if (story is PocketRecommendedStory) {

@ -6,11 +6,14 @@ package org.mozilla.fenix.home.recentbookmarks.controller
import androidx.navigation.NavController
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineSession.LoadUrlFlags.Companion.ALLOW_JAVASCRIPT_URL
import mozilla.components.feature.tabs.TabsUseCases
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.RecentBookmarks
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.home.HomeFragmentDirections
@ -46,15 +49,27 @@ class DefaultRecentBookmarksController(
private val activity: HomeActivity,
private val navController: NavController,
private val appStore: AppStore,
private val browserStore: BrowserStore,
private val selectTabUseCase: TabsUseCases.SelectTabUseCase,
) : RecentBookmarksController {
override fun handleBookmarkClicked(bookmark: RecentBookmark) {
activity.openToBrowserAndLoad(
searchTermOrURL = bookmark.url!!,
newTab = true,
from = BrowserDirection.FromHome,
flags = EngineSession.LoadUrlFlags.select(ALLOW_JAVASCRIPT_URL),
)
val bookmarkTab = browserStore.state.tabs.firstOrNull {
it.content.url == bookmark.url
}
if (bookmarkTab != null) {
selectTabUseCase.invoke(bookmarkTab.id)
navController.navigate(R.id.browserFragment)
} else {
activity.openToBrowserAndLoad(
searchTermOrURL = bookmark.url!!,
newTab = true,
from = BrowserDirection.FromHome,
flags = EngineSession.LoadUrlFlags.select(ALLOW_JAVASCRIPT_URL),
)
}
RecentBookmarks.bookmarkClicked.add()
}

@ -84,7 +84,7 @@ class DefaultBookmarkController(
private val showSnackbar: (String) -> Unit,
private val deleteBookmarkNodes: (Set<BookmarkNode>, BookmarkRemoveType) -> Unit,
private val deleteBookmarkFolder: (Set<BookmarkNode>) -> Unit,
private val showTabTray: () -> Unit,
private val showTabTray: (Boolean) -> Unit,
private val warnLargeOpenAll: (Int, () -> Unit) -> Unit,
private val settings: Settings,
) : BookmarkController {
@ -163,7 +163,7 @@ class DefaultBookmarkController(
override fun handleOpeningBookmark(item: BookmarkNode, mode: BrowsingMode) {
openInNewTab(item.url!!, mode)
showTabTray()
showTabTray(mode.isPrivate)
}
private fun extractURLsFromTree(node: BookmarkNode): MutableList<String> {
@ -199,7 +199,7 @@ class DefaultBookmarkController(
}
activity.browsingModeManager.mode =
BrowsingMode.fromBoolean(mode == BrowsingMode.Private)
showTabTray()
showTabTray(mode.isPrivate)
}
// Warn user if more than maximum number of bookmarks are being opened

@ -55,6 +55,7 @@ import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.setTextColor
import org.mozilla.fenix.library.LibraryPageFragment
import org.mozilla.fenix.tabstray.Page
import org.mozilla.fenix.utils.allowUndo
/**
@ -227,7 +228,7 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
R.id.open_bookmarks_in_private_tabs_multi_select -> {
openItemsInNewTab(private = true) { node -> node.url }
showTabTray()
showTabTray(openInPrivate = true)
BookmarksManagement.openInPrivateTabs.record(NoExtras())
true
}
@ -251,8 +252,16 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
}
}
private fun showTabTray() {
navigateToBookmarkFragment(BookmarkFragmentDirections.actionGlobalTabsTrayFragment())
private fun showTabTray(openInPrivate: Boolean = false) {
navigateToBookmarkFragment(
BookmarkFragmentDirections.actionGlobalTabsTrayFragment(
page = if (openInPrivate) {
Page.PrivateTabs
} else {
Page.NormalTabs
},
),
)
}
private fun navigateToBookmarkFragment(directions: NavDirections) {

@ -16,6 +16,7 @@ import org.mozilla.fenix.selection.SelectionHolder
*/
class HistoryAdapter(
private val historyInteractor: HistoryInteractor,
private val store: HistoryFragmentStore,
private val onEmptyStateChanged: (Boolean) -> Unit,
) : PagingDataAdapter<History, HistoryListItemViewHolder>(historyDiffCallback),
SelectionHolder<History> {
@ -38,7 +39,12 @@ class HistoryAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryListItemViewHolder {
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return HistoryListItemViewHolder(view, historyInteractor, this)
return HistoryListItemViewHolder(
view = view,
historyInteractor = historyInteractor,
selectionHolder = this,
store = store,
)
}
fun updateMode(mode: HistoryFragmentState.Mode) {

@ -41,6 +41,7 @@ import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.ktx.kotlin.toShortUrl
import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.NavHostActivity
import org.mozilla.fenix.R
@ -55,7 +56,11 @@ import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.setTextColor
import org.mozilla.fenix.home.Mode
import org.mozilla.fenix.library.LibraryPageFragment
import org.mozilla.fenix.library.history.state.HistoryNavigationMiddleware
import org.mozilla.fenix.library.history.state.HistoryTelemetryMiddleware
import org.mozilla.fenix.tabstray.Page
import org.mozilla.fenix.utils.allowUndo
import org.mozilla.fenix.GleanMetrics.History as GleanHistory
@ -91,12 +96,15 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler,
val view = binding.root
historyStore = StoreProvider.get(this) {
HistoryFragmentStore(
HistoryFragmentState(
items = listOf(),
mode = HistoryFragmentState.Mode.Normal,
pendingDeletionItems = emptySet(),
isEmpty = false,
isDeletingItems = false,
initialState = HistoryFragmentState.initial,
middleware = listOf(
HistoryNavigationMiddleware(
navController = findNavController(),
openToBrowser = ::openItem,
),
HistoryTelemetryMiddleware(
isInPrivateMode = requireComponents.appStore.state.mode == Mode.Private,
),
),
)
}
@ -127,6 +135,7 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler,
HistoryFragmentAction.ChangeEmptyState(isEmpty = true),
)
},
store = historyStore,
onEmptyStateChanged = {
historyStore.dispatch(
HistoryFragmentAction.ChangeEmptyState(it),
@ -295,7 +304,7 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler,
supportActionBar?.hide()
}
showTabTray()
showTabTray(openInPrivate = true)
historyStore.dispatch(HistoryFragmentAction.ExitEditMode)
true
}
@ -311,10 +320,16 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler,
else -> false
}
private fun showTabTray() {
private fun showTabTray(openInPrivate: Boolean = false) {
findNavController().nav(
R.id.historyFragment,
HistoryFragmentDirections.actionGlobalTabsTrayFragment(),
HistoryFragmentDirections.actionGlobalTabsTrayFragment(
page = if (openInPrivate) {
Page.PrivateTabs
} else {
Page.NormalTabs
},
),
)
}
@ -346,13 +361,16 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler,
}
private fun openItem(item: History.Regular) {
GleanHistory.openedItem.record(
GleanHistory.OpenedItemExtra(
isRemote = item.isRemote,
timeGroup = item.historyTimeGroup.toString(),
isPrivate = (activity as HomeActivity).browsingModeManager.mode == BrowsingMode.Private,
),
)
// This telemetry is recorded by the middleware if the refactor is enabled
if (!FeatureFlags.historyFragmentLibStateRefactor) {
GleanHistory.openedItem.record(
GleanHistory.OpenedItemExtra(
isRemote = item.isRemote,
timeGroup = item.historyTimeGroup.toString(),
isPrivate = (activity as HomeActivity).browsingModeManager.mode == BrowsingMode.Private,
),
)
}
(activity as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = item.url,

@ -9,6 +9,7 @@ import kotlinx.parcelize.Parcelize
import mozilla.components.concept.storage.HistoryMetadata
import mozilla.components.concept.storage.HistoryMetadataKey
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
@ -110,8 +111,11 @@ fun HistoryMetadata.toHistoryMetadata(position: Int): History.Metadata {
/**
* The [Store] for holding the [HistoryFragmentState] and applying [HistoryFragmentAction]s.
*/
class HistoryFragmentStore(initialState: HistoryFragmentState) :
Store<HistoryFragmentState, HistoryFragmentAction>(initialState, ::historyStateReducer)
class HistoryFragmentStore(
initialState: HistoryFragmentState,
middleware: List<Middleware<HistoryFragmentState, HistoryFragmentAction>> = listOf(),
) :
Store<HistoryFragmentState, HistoryFragmentAction>(initialState, ::historyStateReducer, middleware)
/**
* Actions to dispatch through the `HistoryStore` to modify `HistoryState` through the reducer.
@ -121,6 +125,20 @@ sealed class HistoryFragmentAction : Action {
data class AddItemForRemoval(val item: History) : HistoryFragmentAction()
data class RemoveItemForRemoval(val item: History) : HistoryFragmentAction()
/**
* A [History] item has been clicked by a user.
*
* @property item The history item clicked.
*/
data class HistoryItemClicked(val item: History) : HistoryFragmentAction()
/**
* A [History] item has been long-clicked by a user.
*
* @property item The history item long-clicked.
*/
data class HistoryItemLongClicked(val item: History) : HistoryFragmentAction()
/**
* Updates the empty state of [org.mozilla.fenix.library.history.HistoryView].
*/
@ -157,6 +175,16 @@ data class HistoryFragmentState(
object Syncing : Mode()
data class Editing(override val selectedItems: Set<History>) : Mode()
}
companion object {
val initial = HistoryFragmentState(
items = listOf(),
mode = Mode.Normal,
pendingDeletionItems = emptySet(),
isEmpty = false,
isDeletingItems = false,
)
}
}
/**
@ -188,5 +216,38 @@ private fun historyStateReducer(
is HistoryFragmentAction.UpdatePendingDeletionItems -> state.copy(
pendingDeletionItems = action.pendingDeletionItems,
)
is HistoryFragmentAction.HistoryItemClicked -> {
if (state.mode.selectedItems.isEmpty() || state.mode is HistoryFragmentState.Mode.Syncing) {
state
} else {
if (state.mode.selectedItems.contains(action.item)) {
val selected = state.mode.selectedItems - action.item
state.copy(
mode = if (selected.isEmpty()) {
HistoryFragmentState.Mode.Normal
} else {
HistoryFragmentState.Mode.Editing(selected)
},
)
} else {
state.copy(
mode = HistoryFragmentState.Mode.Editing(
state.mode.selectedItems + action.item,
),
)
}
}
}
is HistoryFragmentAction.HistoryItemLongClicked -> {
if (state.mode == HistoryFragmentState.Mode.Syncing) {
state
} else {
state.copy(
mode = HistoryFragmentState.Mode.Editing(
state.mode.selectedItems + action.item,
),
)
}
}
}
}

@ -24,6 +24,7 @@ import org.mozilla.fenix.theme.ThemeManager
class HistoryView(
container: ViewGroup,
val interactor: HistoryInteractor,
val store: HistoryFragmentStore,
val onZeroItemsLoaded: () -> Unit,
val onEmptyStateChanged: (Boolean) -> Unit,
) : LibraryPageView(container), UserInteractionHandler {
@ -37,7 +38,7 @@ class HistoryView(
var mode: HistoryFragmentState.Mode = HistoryFragmentState.Mode.Normal
private set
val historyAdapter = HistoryAdapter(interactor) { isEmpty ->
val historyAdapter = HistoryAdapter(interactor, store) { isEmpty ->
onEmptyStateChanged(isEmpty)
}.apply {
addLoadStateListener {

@ -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.library.history.state
import androidx.navigation.NavController
import androidx.navigation.NavOptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext
import org.mozilla.fenix.R
import org.mozilla.fenix.library.history.History
import org.mozilla.fenix.library.history.HistoryFragmentAction
import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.library.history.HistoryFragmentState
/**
* A [Middleware] for initiating navigation events based on [HistoryFragmentAction]s that are
* dispatched to the [HistoryFragmentStore].
*
* @property navController [NavController] for handling navigation events
* @property openToBrowser Callback to open history items in a browser window.
*/
class HistoryNavigationMiddleware(
private val navController: NavController,
private val openToBrowser: (item: History.Regular) -> Unit,
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main),
) : Middleware<HistoryFragmentState, HistoryFragmentAction> {
override fun invoke(
context: MiddlewareContext<HistoryFragmentState, HistoryFragmentAction>,
next: (HistoryFragmentAction) -> Unit,
action: HistoryFragmentAction,
) {
// Read the current state before letting the chain process the action, so that clicks are
// treated correctly in reference to the number of selected items.
val currentState = context.state
next(action)
scope.launch {
when (action) {
is HistoryFragmentAction.HistoryItemClicked -> {
if (currentState.mode.selectedItems.isEmpty()) {
when (val item = action.item) {
is History.Regular -> openToBrowser(item)
is History.Group -> {
navController.navigate(
HistoryFragmentDirections.actionGlobalHistoryMetadataGroup(
title = item.title,
historyMetadataItems = item.items.toTypedArray(),
),
NavOptions.Builder()
.setPopUpTo(R.id.historyMetadataGroupFragment, true)
.build(),
)
}
else -> Unit
}
}
}
else -> Unit
}
}
}
}

@ -0,0 +1,52 @@
/* 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.library.history.state
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext
import mozilla.components.service.glean.private.NoExtras
import org.mozilla.fenix.library.history.History
import org.mozilla.fenix.library.history.HistoryFragmentAction
import org.mozilla.fenix.library.history.HistoryFragmentState
import org.mozilla.fenix.GleanMetrics.History as GleanHistory
/**
* A [Middleware] for recording telemetry based on [HistoryFragmentAction]s that are dispatched to
* the [HistoryFragmentStore].
*
* @property isInPrivateMode Whether the app is currently in private browsing mode.
*/
class HistoryTelemetryMiddleware(
private val isInPrivateMode: Boolean,
) : Middleware<HistoryFragmentState, HistoryFragmentAction> {
override fun invoke(
context: MiddlewareContext<HistoryFragmentState, HistoryFragmentAction>,
next: (HistoryFragmentAction) -> Unit,
action: HistoryFragmentAction,
) {
val currentState = context.state
next(action)
when (action) {
is HistoryFragmentAction.HistoryItemClicked -> {
if (currentState.mode.selectedItems.isEmpty()) {
when (val item = action.item) {
is History.Regular -> {
GleanHistory.openedItem.record(
GleanHistory.OpenedItemExtra(
isRemote = item.isRemote,
timeGroup = item.historyTimeGroup.toString(),
isPrivate = isInPrivateMode,
),
)
}
is History.Group -> GleanHistory.searchTermGroupTapped.record(NoExtras())
else -> Unit
}
}
}
else -> Unit
}
}
}

@ -7,13 +7,16 @@ package org.mozilla.fenix.library.history.viewholders
import android.view.View
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.HistoryListItemBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.hideAndDisable
import org.mozilla.fenix.ext.showAndEnable
import org.mozilla.fenix.library.history.History
import org.mozilla.fenix.library.history.HistoryFragmentAction
import org.mozilla.fenix.library.history.HistoryFragmentState
import org.mozilla.fenix.library.history.HistoryFragmentStore
import org.mozilla.fenix.library.history.HistoryInteractor
import org.mozilla.fenix.library.history.HistoryItemTimeGroup
import org.mozilla.fenix.selection.SelectionHolder
@ -22,6 +25,7 @@ class HistoryListItemViewHolder(
view: View,
private val historyInteractor: HistoryInteractor,
private val selectionHolder: SelectionHolder<History>,
private val store: HistoryFragmentStore,
) : RecyclerView.ViewHolder(view) {
private var item: History? = null
@ -85,7 +89,18 @@ class HistoryListItemViewHolder(
val headerText = timeGroup?.humanReadable(itemView.context)
toggleHeader(headerText)
binding.historyLayout.setSelectionInteractor(item, selectionHolder, historyInteractor)
if (FeatureFlags.historyFragmentLibStateRefactor) {
binding.historyLayout.setOnClickListener {
store.dispatch(HistoryFragmentAction.HistoryItemClicked(item))
}
binding.historyLayout.setOnLongClickListener {
store.dispatch(HistoryFragmentAction.HistoryItemLongClicked(item))
true
}
} else {
binding.historyLayout.setSelectionInteractor(item, selectionHolder, historyInteractor)
}
binding.historyLayout.changeSelected(item in selectionHolder.selectedItems)
if (item is History.Regular &&

@ -46,6 +46,7 @@ import org.mozilla.fenix.library.historymetadata.controller.DefaultHistoryMetada
import org.mozilla.fenix.library.historymetadata.interactor.DefaultHistoryMetadataGroupInteractor
import org.mozilla.fenix.library.historymetadata.interactor.HistoryMetadataGroupInteractor
import org.mozilla.fenix.library.historymetadata.view.HistoryMetadataGroupView
import org.mozilla.fenix.tabstray.Page
import org.mozilla.fenix.utils.allowUndo
/**
@ -191,7 +192,7 @@ class HistoryMetadataGroupFragment :
supportActionBar?.hide()
}
showTabTray()
showTabTray(openInPrivate = true)
true
}
R.id.history_delete -> {
@ -241,10 +242,16 @@ class HistoryMetadataGroupFragment :
}
}
private fun showTabTray() {
private fun showTabTray(openInPrivate: Boolean = false) {
findNavController().nav(
R.id.historyMetadataGroupFragment,
HistoryMetadataGroupFragmentDirections.actionGlobalTabsTrayFragment(),
HistoryMetadataGroupFragmentDirections.actionGlobalTabsTrayFragment(
page = if (openInPrivate) {
Page.PrivateTabs
} else {
Page.NormalTabs
},
),
)
}

@ -11,7 +11,9 @@ import mozilla.components.service.nimbus.ui.NimbusBranchesAdapterDelegate
import org.mozilla.experiments.nimbus.Branch
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.ext.navigateWithBreadcrumb
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.NimbusBranchesAction
import org.mozilla.fenix.nimbus.NimbusBranchesFragmentDirections
@ -51,9 +53,12 @@ class NimbusBranchesController(
)
.setText(snackbarText)
.setAction(buttonText) {
navController.navigate(
NimbusBranchesFragmentDirections
navController.navigateWithBreadcrumb(
directions = NimbusBranchesFragmentDirections
.actionNimbusBranchesFragmentToDataChoicesFragment(),
navigateFrom = "NimbusBranchesController",
navigateTo = "ActionNimbusBranchesFragmentToDataChoicesFragment",
crashReporter = context.components.analytics.crashReporter,
)
}
.show()

@ -100,6 +100,7 @@ class OnboardingFragment : Fragment() {
activity = activity,
navController = findNavController(),
onboarding = requireComponents.fenixOnboarding,
crashReporter = requireComponents.analytics.crashReporter,
),
privateBrowsingController = DefaultPrivateBrowsingController(
activity = activity,

@ -5,8 +5,10 @@
package org.mozilla.fenix.onboarding.controller
import androidx.navigation.NavController
import mozilla.components.lib.crash.CrashReporter
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.ext.navigateWithBreadcrumb
import org.mozilla.fenix.onboarding.FenixOnboarding
import org.mozilla.fenix.onboarding.OnboardingFragmentDirections
import org.mozilla.fenix.onboarding.interactor.OnboardingInteractor
@ -34,13 +36,17 @@ class DefaultOnboardingController(
private val activity: HomeActivity,
private val navController: NavController,
private val onboarding: FenixOnboarding,
private val crashReporter: CrashReporter,
) : OnboardingController {
override fun handleFinishOnboarding(focusOnAddressBar: Boolean) {
onboarding.finish()
navController.navigate(
OnboardingFragmentDirections.actionHome(focusOnAddressBar = focusOnAddressBar),
navController.navigateWithBreadcrumb(
directions = OnboardingFragmentDirections.actionHome(focusOnAddressBar = focusOnAddressBar),
navigateFrom = "OnboardingFragment",
navigateTo = "ActionHome",
crashReporter = crashReporter,
)
if (focusOnAddressBar) {

@ -104,7 +104,7 @@ fun OnboardingPage(
modifier = Modifier.align(Alignment.End),
) {
Icon(
painter = painterResource(id = R.drawable.mozac_ic_close),
painter = painterResource(id = R.drawable.mozac_ic_cross_24),
contentDescription = stringResource(R.string.onboarding_home_content_description_close_button),
tint = FirefoxTheme.colors.iconPrimary,
)

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save