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

pull/636/head
akliuxingyuan 11 months ago
commit dbcc0ad145

@ -1 +1 @@
Subproject commit 53f2ba8b6bbd3e8a720e5b90b073eabcc854094f
Subproject commit ac015fe2d5ef0700f93e40b62094f1cf79edcf86

@ -1,12 +1,4 @@
---
client-deduplication:
description: A feature to control the sending of the client-deduplication ping.
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "If true, the ping will be sent."
cookie-banners:
description: Features for cookie banner handling.
hasExposure: true
@ -117,6 +109,17 @@ onboarding:
order:
type: json
description: Determines the order of the onboarding page panels
pdfjs:
description: PDF.js features
hasExposure: true
exposureDescription: ""
variables:
download-button:
type: boolean
description: Download button
open-in-app-button:
type: boolean
description: Open in app button
pre-permission-notification-prompt:
description: A feature that shows the pre-permission notification prompt.
hasExposure: true
@ -144,6 +147,14 @@ search-term-groups:
enabled:
type: boolean
description: "If true, the feature shows up on the homescreen and on the new tab screen."
toolbar:
description: The searchbar/awesomebar that user uses to search.
hasExposure: true
exposureDescription: ""
variables:
toolbar-position-top:
type: boolean
description: "If true, toolbar appears at top of the screen."
unified-search:
description: A feature allowing user to easily search for specified results directly in the search bar.
hasExposure: true

@ -2,8 +2,8 @@ import org.apache.tools.ant.util.StringUtils
import org.mozilla.fenix.gradle.tasks.ApkSizeTask
plugins {
id "com.jetbrains.python.envs" version "0.0.26"
id "com.google.protobuf" version "0.8.19"
id "com.jetbrains.python.envs" version "$python_envs_plugin"
id "com.google.protobuf" version "$protobuf_plugin"
}
apply plugin: 'com.android.application'
@ -32,11 +32,6 @@ android {
testBuildType project.property("testBuildType")
}
// This allows overriding the target activity for MozillaOnline builds, which happens
// as part of the defaultConfig below, and applies to all other configurations (Nightly,
// Beta, and Release.)
def targetActivity = "HomeActivity"
defaultConfig {
applicationId "io.github.forkmaintainers"
minSdkVersion Config.minSdkVersion
@ -68,9 +63,14 @@ android {
"}"
// This should be the base URL used to call the AMO API.
buildConfigField "String", "AMO_SERVER_URL", "\"https://services.addons.mozilla.org\""
def deepLinkSchemeValue = "fenix-dev"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
// This allows overriding the target activity for MozillaOnline builds, which happens
// as part of the defaultConfig below.
def targetActivity = "HomeActivity"
// Build flag for "Mozilla Online" variants. See `Config.isMozillaOnline`.
if (project.hasProperty("mozillaOnline") || gradle.hasProperty("localProperties.mozillaOnline")) {
buildConfigField "boolean", "MOZILLA_ONLINE", "true"
@ -78,6 +78,7 @@ android {
} else {
buildConfigField "boolean", "MOZILLA_ONLINE", "false"
}
manifestPlaceholders = [
"targetActivity": targetActivity,
"deepLinkScheme": deepLinkSchemeValue
@ -118,46 +119,43 @@ android {
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
def deepLinkSchemeValue = "fenix-nightly"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = [
"deepLinkScheme": deepLinkSchemeValue,
"targetActivity": targetActivity
]
manifestPlaceholders.putAll([
"deepLinkScheme": deepLinkSchemeValue
])
}
beta releaseTemplate >> {
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
applicationIdSuffix ".firefox_beta"
def deepLinkSchemeValue = "fenix-beta"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = [
manifestPlaceholders.putAll([
// This release type is meant to replace Firefox (Beta channel) and therefore needs to inherit
// its sharedUserId for all eternity. See:
// https://searchfox.org/mozilla-central/search?q=moz_android_shared_id&case=false&regexp=false&path=
// https://searchfox.org/mozilla-esr68/search?q=moz_android_shared_id&case=false&regexp=false&path=
// Shipping an app update without sharedUserId can have
// fatal consequences. For example see:
// - https://issuetracker.google.com/issues/36924841
// - https://issuetracker.google.com/issues/36905922
"sharedUserId": "org.mozilla.firefox.sharedID",
"deepLinkScheme": deepLinkSchemeValue,
"targetActivity": targetActivity
]
])
}
release releaseTemplate >> {
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
applicationIdSuffix ".firefox"
def deepLinkSchemeValue = "fenix"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = [
manifestPlaceholders.putAll([
// This release type is meant to replace Firefox (Release channel) and therefore needs to inherit
// its sharedUserId for all eternity. See:
// https://searchfox.org/mozilla-central/search?q=moz_android_shared_id&case=false&regexp=false&path=
// https://searchfox.org/mozilla-esr68/search?q=moz_android_shared_id&case=false&regexp=false&path=
// Shipping an app update without sharedUserId can have
// fatal consequences. For example see:
// - https://issuetracker.google.com/issues/36924841
// - https://issuetracker.google.com/issues/36905922
"sharedUserId": "org.mozilla.firefox.sharedID",
"deepLinkScheme": deepLinkSchemeValue,
"targetActivity": targetActivity,
]
])
}
forkDebug {
shrinkResources false
@ -260,7 +258,8 @@ android {
}
packagingOptions {
resources {
excludes += ['META-INF/atomicfu.kotlin_module', 'META-INF/AL2.0', 'META-INF/LGPL2.1']
excludes += ['META-INF/atomicfu.kotlin_module', 'META-INF/AL2.0', 'META-INF/LGPL2.1',
'META-INF/LICENSE.md', 'META-INF/LICENSE-notice.md']
}
}
@ -513,7 +512,6 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
freeCompilerArgs += [
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=com.google.accompanist.pager.ExperimentalPagerApi",
]
}
}
@ -528,7 +526,6 @@ dependencies {
implementation FenixDependencies.androidx_constraintlayout
implementation FenixDependencies.androidx_coordinatorlayout
implementation FenixDependencies.google_accompanist_drawablepainter
implementation FenixDependencies.google_accompanist_pager
implementation FenixDependencies.sentry
@ -655,9 +652,9 @@ dependencies {
implementation FenixDependencies.google_material
androidTestImplementation FenixDependencies.uiautomator
androidTestImplementation "tools.fastlane:screengrab:2.0.0"
androidTestImplementation FenixDependencies.fastlane
// This Falcon version is added to maven central now required for Screengrab
androidTestImplementation 'com.jraska:falcon:2.2.0'
androidTestImplementation FenixDependencies.falcon
androidTestImplementation FenixDependencies.androidx_compose_ui_test
@ -700,7 +697,7 @@ dependencies {
exclude group: 'org.apache.maven'
}
testImplementation 'org.apache.maven:maven-ant-tasks:2.1.3'
testImplementation FenixDependencies.maven_ant_tasks
implementation project(':support-rusthttp')
androidTestImplementation FenixDependencies.mockk_android
@ -714,14 +711,8 @@ dependencies {
}
protobuf {
// Mac M1 workaround until we can bump the version. Dependent on A-S.
// See https://github.com/mozilla-mobile/fenix/issues/22321
protoc {
if (osdetector.os == "osx") {
artifact = "${FenixDependencies.protobuf_compiler}:osx-x86_64"
} else {
artifact = FenixDependencies.protobuf_compiler
}
artifact = FenixDependencies.protobuf_compiler
}
// Generates the java Protobuf-lite code for the Protobufs in this project. See
@ -745,7 +736,7 @@ if (project.hasProperty("coverage")) {
}
jacoco {
toolVersion = "0.8.7"
toolVersion = FenixVersions.jacoco
}
android.applicationVariants.all { variant ->
@ -884,20 +875,6 @@ if (gradle.hasProperty('localProperties.dependencySubstitutions.geckoviewTopsrcd
apply from: "${topsrcdir}/substitute-local-geckoview.gradle"
}
def appServicesSrcDir = null
if (gradle.hasProperty('localProperties.autoPublish.application-services.dir')) {
appServicesSrcDir = gradle.getProperty('localProperties.autoPublish.application-services.dir')
} else if (gradle.hasProperty('localProperties.branchBuild.application-services.dir')) {
appServicesSrcDir = gradle.getProperty('localProperties.branchBuild.application-services.dir')
}
if (appServicesSrcDir) {
if (appServicesSrcDir.startsWith("/")) {
apply from: "${appServicesSrcDir}/build-scripts/substitute-local-appservices.gradle"
} else {
apply from: "../${appServicesSrcDir}/build-scripts/substitute-local-appservices.gradle"
}
}
if (gradle.hasProperty('localProperties.autoPublish.glean.dir')) {
ext.gleanSrcDir = gradle."localProperties.autoPublish.glean.dir"
apply from: "../${gleanSrcDir}/build-scripts/substitute-local-glean.gradle"

@ -258,7 +258,6 @@ events:
send_in_pings:
- metrics
- baseline
- client-deduplication
bugs:
- https://github.com/mozilla-mobile/fenix/issues/17089
data_reviews:
@ -1837,7 +1836,6 @@ metrics:
send_in_pings:
- metrics
- baseline
- client-deduplication
bugs:
- https://github.com/mozilla-mobile/fenix/issues/1158
- https://github.com/mozilla-mobile/fenix/issues/6556
@ -2025,30 +2023,6 @@ metrics:
metadata:
tags:
- Telemetry
toolbar_position:
type: string
lifetime: application
description: |
A string that indicates the new position of the toolbar TOP or BOTTOM
send_in_pings:
- metrics
bugs:
- https://github.com/mozilla-mobile/fenix/issues/6054
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/6608
- https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068
- https://github.com/mozilla-mobile/fenix/pull/19924#issuecomment-861423789
- https://github.com/mozilla-mobile/fenix/pull/20517#pullrequestreview-718069041
- https://github.com/mozilla-mobile/fenix/pull/23453#issuecomment-1024694220
- https://github.com/mozilla-mobile/fenix/pull/28502
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 124
metadata:
tags:
- Toolbar
close_tab_setting:
type: string
lifetime: application
@ -2133,7 +2107,6 @@ metrics:
please see `has_open_tabs`.
send_in_pings:
- metrics
- client-deduplication
bugs:
- https://github.com/mozilla-mobile/fenix/issues/11479
data_reviews:
@ -2951,7 +2924,6 @@ search.default_engine:
send_in_pings:
- metrics
- baseline
- client-deduplication
bugs:
- https://github.com/mozilla-mobile/fenix/issues/800
data_reviews:
@ -2981,7 +2953,6 @@ search.default_engine:
send_in_pings:
- metrics
- baseline
- client-deduplication
bugs:
- https://github.com/mozilla-mobile/fenix/issues/800
data_reviews:
@ -3567,8 +3538,6 @@ sync_auth:
notification_emails:
- android-probes@mozilla.com
- cgordon@mozilla.com
send_in_pings:
- client-deduplication
expires: never
metadata:
tags:
@ -3868,11 +3837,12 @@ history:
- https://github.com/mozilla-mobile/fenix/issues/26101
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/26153
- https://github.com/mozilla-mobile/firefox-android/pull/2225
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 117
expires: 123
metadata:
tags:
- History
@ -3884,11 +3854,12 @@ history:
- https://github.com/mozilla-mobile/fenix/issues/26101
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/26153
- https://github.com/mozilla-mobile/firefox-android/pull/2225
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 117
expires: 123
metadata:
tags:
- History
@ -3900,11 +3871,12 @@ history:
- https://github.com/mozilla-mobile/fenix/issues/26101
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/26153
- https://github.com/mozilla-mobile/firefox-android/pull/2225
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 117
expires: 123
metadata:
tags:
- History
@ -3916,11 +3888,12 @@ history:
- https://github.com/mozilla-mobile/fenix/issues/26101
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/26153
- https://github.com/mozilla-mobile/firefox-android/pull/2225
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 117
expires: 123
metadata:
tags:
- History
@ -7001,7 +6974,6 @@ browser.search:
send_in_pings:
- metrics
- baseline
- client-deduplication
bugs:
- https://github.com/mozilla-mobile/fenix/issues/6558
- https://github.com/mozilla-mobile/fenix/issues/28010
@ -7036,7 +7008,6 @@ browser.search:
send_in_pings:
- metrics
- baseline
- client-deduplication
bugs:
- https://github.com/mozilla-mobile/fenix/issues/6558
- https://github.com/mozilla-mobile/fenix/issues/28010
@ -7062,7 +7033,6 @@ browser.search:
send_in_pings:
- metrics
- baseline
- client-deduplication
bugs:
- https://github.com/mozilla-mobile/fenix/issues/6557
data_reviews:
@ -9476,56 +9446,6 @@ review_prompt:
data_sensitivity:
- interaction
expires: 121
client_deduplication:
valid_advertising_id:
type: boolean
description: |
Whether or not we get a valid advertising ID from the device.
send_in_pings:
- client-deduplication
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1817029
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1813195#c11
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
- fbertsch@mozilla.com
expires: 122
experiment_timeframe:
type: string
description: |
A string we use to identify which run of the experiment this is.
send_in_pings:
- client-deduplication
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1817029
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1813195#c11
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
- fbertsch@mozilla.com
expires: 122
hashed_gaid:
type: string
lifetime: ping
description: |
A hashed and salted version of the Google Advertising ID from the device.
send_in_pings:
- client-deduplication
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1817029
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1813195#c11
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
- fbertsch@mozilla.com
expires: 122
private_browsing_shortcut_cfr:
add_shortcut:

@ -133,8 +133,24 @@ import:
max-display-count: 1
notification-config:
refresh-interval: 120 # minutes (2 hours)
- path: ../../android-components/components/browser/engine-gecko/geckoview.fml.yaml
channel: release
features:
pdfjs:
- channel: developer
value: {
download-button: true,
open-in-app-button: true
}
features:
toolbar:
description: The searchbar/awesomebar that user uses to search.
variables:
toolbar-position-top:
description: If true, toolbar appears at top of the screen.
type: Boolean
default: false
homescreen:
description: The homescreen that the user goes to when they press home or new tab.
variables:
@ -253,29 +269,7 @@ features:
enabled:
description: If true, the feature shows up in the search bar.
type: Boolean
default: false
defaults:
- channel: nightly
value:
enabled: true
- channel: developer
value:
enabled: true
client-deduplication:
description: A feature to control the sending of the client-deduplication ping.
variables:
enabled:
description: If true, the ping will be sent.
type: Boolean
default: false
defaults:
- channel: nightly
value:
enabled: false
- channel: developer
value:
enabled: false
default: true
growth-data:
description: A feature measuring campaign growth data

@ -65,24 +65,6 @@ spoc:
notification_emails:
- android-probes@mozilla.com
client-deduplication:
description: |
Contains data to help identify if client IDs are being regenerated
erroneously.
include_client_id: true
reasons:
active: |
The ping is being sent when the app is coming to the foreground.
inactive: |
The ping is being sent when the app is going to the background.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1817029
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1813195#c11
notification_emails:
- android-probes@mozilla.com
- fbertsch@mozilla.com
cookie-banner-report-site:
description: |
This ping is needed when the cookie banner reducer doesn't work on

@ -77,6 +77,11 @@ interface FeatureSettingsHelper {
*/
var isOpenInAppBannerEnabled: Boolean
/**
* Enable or disable the Tabs Tray to Compose rewrite.
*/
var tabsTrayRewriteEnabled: Boolean
fun applyFlagUpdates()
fun resetAllFeatureFlags()

@ -37,6 +37,8 @@ class FeatureSettingsHelperDelegate : FeatureSettingsHelper {
isCookieBannerReductionDialogEnabled = !settings.userOptOutOfReEngageCookieBannerDialog,
isOpenInAppBannerEnabled = settings.shouldShowOpenInAppBanner,
etpPolicy = getETPPolicy(settings),
tabsTrayRewriteEnabled = settings.enableTabsTrayToCompose,
newSearchSettingsEnabled = false,
)
/**
@ -64,6 +66,7 @@ class FeatureSettingsHelperDelegate : FeatureSettingsHelper {
override var isCookieBannerReductionDialogEnabled: Boolean by updatedFeatureFlags::isCookieBannerReductionDialogEnabled
override var isOpenInAppBannerEnabled: Boolean by updatedFeatureFlags::isOpenInAppBannerEnabled
override var etpPolicy: ETPPolicy by updatedFeatureFlags::etpPolicy
override var tabsTrayRewriteEnabled: Boolean by updatedFeatureFlags::tabsTrayRewriteEnabled
override fun applyFlagUpdates() {
applyFeatureFlags(updatedFeatureFlags)
@ -89,6 +92,8 @@ class FeatureSettingsHelperDelegate : FeatureSettingsHelper {
settings.deleteSitePermissions = featureFlags.isDeleteSitePermissionsEnabled
settings.userOptOutOfReEngageCookieBannerDialog = !featureFlags.isCookieBannerReductionDialogEnabled
settings.shouldShowOpenInAppBanner = featureFlags.isOpenInAppBannerEnabled
settings.enableTabsTrayToCompose = featureFlags.tabsTrayRewriteEnabled
settings.enableUnifiedSearchSettingsUI = featureFlags.newSearchSettingsEnabled
setETPPolicy(featureFlags.etpPolicy)
}
}
@ -108,6 +113,8 @@ private data class FeatureFlags(
var isCookieBannerReductionDialogEnabled: Boolean,
var isOpenInAppBannerEnabled: Boolean,
var etpPolicy: ETPPolicy,
var tabsTrayRewriteEnabled: Boolean,
var newSearchSettingsEnabled: Boolean,
)
internal fun getETPPolicy(settings: Settings): ETPPolicy {

@ -8,6 +8,7 @@ package org.mozilla.fenix.helpers
import android.content.Intent
import android.view.ViewConfiguration.getLongPressTimeout
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.espresso.intent.rule.IntentsTestRule
import androidx.test.rule.ActivityTestRule
import androidx.test.uiautomator.UiSelector
@ -17,6 +18,8 @@ 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>
/**
* A [org.junit.Rule] to handle shared test set up for tests on [HomeActivity].
*
@ -51,6 +54,7 @@ class HomeActivityTestRule(
isCookieBannerReductionDialogEnabled: Boolean = !settings.userOptOutOfReEngageCookieBannerDialog,
isOpenInAppBannerEnabled: Boolean = settings.shouldShowOpenInAppBanner,
etpPolicy: ETPPolicy = getETPPolicy(settings),
tabsTrayRewriteEnabled: Boolean = false,
) : this(initialTouchMode, launchActivity, skipOnboarding) {
this.isHomeOnboardingDialogEnabled = isHomeOnboardingDialogEnabled
this.isPocketEnabled = isPocketEnabled
@ -64,6 +68,7 @@ class HomeActivityTestRule(
this.isCookieBannerReductionDialogEnabled = isCookieBannerReductionDialogEnabled
this.isOpenInAppBannerEnabled = isOpenInAppBannerEnabled
this.etpPolicy = etpPolicy
this.tabsTrayRewriteEnabled = tabsTrayRewriteEnabled
}
/**
@ -71,7 +76,7 @@ class HomeActivityTestRule(
*/
fun applySettingsExceptions(settings: (FeatureSettingsHelper) -> Unit) {
FeatureSettingsHelperDelegate().also {
settings(it)
settings(this)
applyFlagUpdates()
}
}
@ -107,10 +112,12 @@ class HomeActivityTestRule(
initialTouchMode: Boolean = false,
launchActivity: Boolean = true,
skipOnboarding: Boolean = false,
tabsTrayRewriteEnabled: Boolean = false,
) = HomeActivityTestRule(
initialTouchMode = initialTouchMode,
launchActivity = launchActivity,
skipOnboarding = skipOnboarding,
tabsTrayRewriteEnabled = tabsTrayRewriteEnabled,
isJumpBackInCFREnabled = false,
isPWAsPromptEnabled = false,
isTCPCFREnabled = false,
@ -155,6 +162,7 @@ class HomeActivityIntentTestRule internal constructor(
isCookieBannerReductionDialogEnabled: Boolean = !settings.userOptOutOfReEngageCookieBannerDialog,
isOpenInAppBannerEnabled: Boolean = settings.shouldShowOpenInAppBanner,
etpPolicy: ETPPolicy = getETPPolicy(settings),
tabsTrayRewriteEnabled: Boolean = false,
) : this(initialTouchMode, launchActivity, skipOnboarding) {
this.isHomeOnboardingDialogEnabled = isHomeOnboardingDialogEnabled
this.isPocketEnabled = isPocketEnabled
@ -168,6 +176,7 @@ class HomeActivityIntentTestRule internal constructor(
this.isCookieBannerReductionDialogEnabled = isCookieBannerReductionDialogEnabled
this.isOpenInAppBannerEnabled = isOpenInAppBannerEnabled
this.etpPolicy = etpPolicy
this.tabsTrayRewriteEnabled = tabsTrayRewriteEnabled
}
private val longTapUserPreference = getLongPressTimeout()
@ -248,10 +257,12 @@ class HomeActivityIntentTestRule internal constructor(
initialTouchMode: Boolean = false,
launchActivity: Boolean = true,
skipOnboarding: Boolean = false,
tabsTrayRewriteEnabled: Boolean = false,
) = HomeActivityIntentTestRule(
initialTouchMode = initialTouchMode,
launchActivity = launchActivity,
skipOnboarding = skipOnboarding,
tabsTrayRewriteEnabled = tabsTrayRewriteEnabled,
isJumpBackInCFREnabled = false,
isPWAsPromptEnabled = false,
isTCPCFREnabled = false,

@ -9,6 +9,7 @@ import androidx.test.uiautomator.UiSelector
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
import org.mozilla.fenix.helpers.TestHelper.mDevice
/**
@ -55,7 +56,7 @@ object MatcherHelper {
}
} else {
for (appItem in appItems) {
assertFalse(appItem.waitForExists(waitingTime))
assertFalse(appItem.waitForExists(waitingTimeShort))
}
}
}
@ -65,7 +66,7 @@ object MatcherHelper {
if (exists) {
assertTrue(appItem.waitForExists(waitingTime))
} else {
assertFalse(appItem.waitForExists(waitingTime))
assertFalse(appItem.waitForExists(waitingTimeShort))
}
}
}
@ -75,7 +76,7 @@ object MatcherHelper {
if (exists) {
assertTrue(appItem.waitForExists(waitingTime))
} else {
assertFalse(appItem.waitForExists(waitingTime))
assertFalse(appItem.waitForExists(waitingTimeShort))
}
}
}
@ -103,7 +104,7 @@ object MatcherHelper {
if (exists) {
assertTrue(appItem.waitForExists(waitingTime))
} else {
assertFalse(appItem.waitForExists(waitingTime))
assertFalse(appItem.waitForExists(waitingTimeShort))
}
}
}

@ -15,6 +15,7 @@ import org.junit.runners.model.Statement
import org.mozilla.fenix.components.PermissionStorage
import org.mozilla.fenix.helpers.IdlingResourceHelper.unregisterAllIdlingResources
import org.mozilla.fenix.helpers.TestHelper.appContext
import org.mozilla.fenix.helpers.TestHelper.setNetworkEnabled
/**
* Rule to retry flaky tests for a given number of times, catching some of the more common exceptions.
@ -34,6 +35,7 @@ class RetryTestRule(private val retryCount: Int = 5) : TestRule {
base.evaluate()
break
} catch (t: AssertionError) {
setNetworkEnabled(true)
unregisterAllIdlingResources()
runBlocking {
permissionStorage.deleteAllSitePermissions()
@ -50,6 +52,7 @@ class RetryTestRule(private val retryCount: Int = 5) : TestRule {
throw t
}
} catch (t: UiObjectNotFoundException) {
setNetworkEnabled(true)
unregisterAllIdlingResources()
runBlocking {
permissionStorage.deleteAllSitePermissions()
@ -58,6 +61,7 @@ class RetryTestRule(private val retryCount: Int = 5) : TestRule {
throw t
}
} catch (t: NoMatchingViewException) {
setNetworkEnabled(true)
unregisterAllIdlingResources()
runBlocking {
permissionStorage.deleteAllSitePermissions()
@ -66,6 +70,7 @@ class RetryTestRule(private val retryCount: Int = 5) : TestRule {
throw t
}
} catch (t: IdlingResourceTimeoutException) {
setNetworkEnabled(true)
unregisterAllIdlingResources()
runBlocking {
permissionStorage.deleteAllSitePermissions()
@ -74,6 +79,7 @@ class RetryTestRule(private val retryCount: Int = 5) : TestRule {
throw t
}
} catch (t: RuntimeException) {
setNetworkEnabled(true)
unregisterAllIdlingResources()
runBlocking {
permissionStorage.deleteAllSitePermissions()
@ -82,6 +88,7 @@ class RetryTestRule(private val retryCount: Int = 5) : TestRule {
throw t
}
} catch (t: NullPointerException) {
setNetworkEnabled(true)
unregisterAllIdlingResources()
runBlocking {
permissionStorage.deleteAllSitePermissions()

@ -76,7 +76,6 @@ import org.mozilla.fenix.utils.IntentUtils
import org.mozilla.gecko.util.ThreadUtils
import java.io.File
import java.util.Locale
import java.util.regex.Pattern
object TestHelper {
@ -138,6 +137,15 @@ object TestHelper {
).waitUntilGone(waitingTime)
}
fun verifySnackBarText(expectedText: String) {
assertTrue(
mDevice.findObject(
UiSelector()
.textContains(expectedText),
).waitForExists(waitingTime),
)
}
fun verifyUrl(urlSubstring: String, resourceName: String, resId: Int) {
waitUntilObjectIsFound(resourceName)
mDevice.findObject(UiSelector().text(urlSubstring)).waitForExists(waitingTime)
@ -334,7 +342,7 @@ object TestHelper {
)
}
fun getStringResource(id: Int) = appContext.resources.getString(id, appName)
fun getStringResource(id: Int, argument: String = appName) = appContext.resources.getString(id, argument)
fun setCustomSearchEngine(searchEngine: SearchEngine) {
with(appContext.components.useCases.searchUseCases) {
@ -345,37 +353,29 @@ object TestHelper {
// Permission allow dialogs differ on various Android APIs
fun grantSystemPermission() {
val whileUsingTheAppPermissionButton: UiObject =
mDevice.findObject(UiSelector().textContains("While using the app"))
val allowPermissionButton: UiObject =
mDevice.findObject(
UiSelector()
.textContains("Allow")
.className("android.widget.Button"),
)
if (Build.VERSION.SDK_INT >= 23) {
if (mDevice.findObject(UiSelector().textContains("While using the app")).waitForExists(
waitingTimeShort,
)
) {
mDevice.findObject(UiSelector().textContains("While using the app")).click()
} else {
mDevice.findObject(
UiSelector()
.textContains("Allow")
.className("android.widget.Button"),
).click()
if (whileUsingTheAppPermissionButton.waitForExists(waitingTimeShort)) {
whileUsingTheAppPermissionButton.click()
} else if (allowPermissionButton.waitForExists(waitingTimeShort)) {
allowPermissionButton.click()
}
}
}
// Permission deny dialogs differ on various Android APIs
fun denyPermission() {
if (Build.VERSION.SDK_INT >= 23) {
mDevice.findObject(
By.text(
when (Build.VERSION.SDK_INT) {
Build.VERSION_CODES.R -> Pattern.compile(
"DENY",
Pattern.CASE_INSENSITIVE,
)
else -> Pattern.compile("Deny", Pattern.CASE_INSENSITIVE)
},
),
).click()
}
mDevice.findObject(UiSelector().textContains("Deny")).waitForExists(waitingTime)
mDevice.findObject(UiSelector().textContains("Deny")).click()
}
fun isTestLab(): Boolean {

@ -19,12 +19,13 @@ 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.HomeActivityTestRule
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
@ -32,6 +33,7 @@ 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
import org.mozilla.fenix.ui.robots.searchScreen
/**
* Tests for verifying basic functionality of bookmarks
@ -48,7 +50,7 @@ class BookmarksTest {
}
@get:Rule
val activityTestRule = HomeActivityTestRule.withDefaultSettingsOverrides()
val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
@Rule
@JvmField
@ -741,4 +743,116 @@ class BookmarksTest {
verifyFolderTitle("My Folder")
}
}
@Test
fun verifySearchBookmarksViewTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
clickSearchButton()
verifyBookmarksSearchBar(true)
verifyBookmarksSearchBarPosition(true)
clickOutsideTheSearchBar()
verifyBookmarksSearchBar(false)
}.goBackToBrowserScreen {
}.openThreeDotMenu {
}.openSettings {
}.openCustomizeSubMenu {
clickTopToolbarToggle()
}
exitMenu()
browserScreen {
}.openThreeDotMenu {
}.openBookmarks {
clickSearchButton()
verifyBookmarksSearchBar(true)
verifyBookmarksSearchBarPosition(false)
dismissBookmarksSearchBarUsingBackButton()
verifyBookmarksSearchBar(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
searchBookmarkedItem(firstWebPage.title)
verifySearchedBookmarkExists(firstWebPage.url.toString(), true)
verifySearchedBookmarkExists(secondWebPage.url.toString(), false)
// Search for invalid term
searchBookmarkedItem("Android")
verifySearchedBookmarkExists(firstWebPage.url.toString(), false)
verifySearchedBookmarkExists(secondWebPage.url.toString(), false)
}
}
@Test
fun verifyVoiceSearchInBookmarksTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
clickSearchButton()
verifyBookmarksSearchBar(true)
}
searchScreen {
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.url) {
}.clickDelete {
verifyBookmarkIsDeleted(firstWebPage.title)
}.openThreeDotMenu(secondWebPage.url) {
}.clickDelete {
verifyBookmarkIsDeleted(secondWebPage.title)
clickSearchButton()
searchBookmarkedItem("generic")
verifySearchedBookmarkExists(firstWebPage.url.toString(), false)
verifySearchedBookmarkExists(secondWebPage.url.toString(), false)
verifySearchedBookmarkExists(thirdWebPage.url.toString(), true)
dismissBookmarksSearchBar()
}.openThreeDotMenu(thirdWebPage.url) {
}.clickDelete {
verifyBookmarkIsDeleted(thirdWebPage.title)
clickSearchButton()
searchBookmarkedItem("generic")
verifySearchedBookmarkExists(thirdWebPage.url.toString(), false)
}
}
}

@ -5,14 +5,18 @@
package org.mozilla.fenix.ui
import androidx.core.net.toUri
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.HomeActivityTestRule
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
import org.mozilla.fenix.helpers.RetryTestRule
import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
import org.mozilla.fenix.helpers.TestHelper.getStringResource
import org.mozilla.fenix.helpers.TestHelper.setNetworkEnabled
import org.mozilla.fenix.ui.robots.browserScreen
@ -28,6 +32,7 @@ class BrowsingErrorPagesTest {
private val unwantedSoftwareWarning =
getStringResource(R.string.mozac_browser_errorpages_safe_browsing_unwanted_uri_title)
private val harmfulSiteWarning = getStringResource(R.string.mozac_browser_errorpages_safe_harmful_uri_title)
private lateinit var mockWebServer: MockWebServer
@get: Rule
val mActivityTestRule = HomeActivityTestRule.withDefaultSettingsOverrides()
@ -36,10 +41,19 @@ class BrowsingErrorPagesTest {
@JvmField
val retryTestRule = RetryTestRule(3)
@Before
fun setUp() {
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@After
fun tearDown() {
// Restoring network connection
setNetworkEnabled(true)
mockWebServer.shutdown()
}
@SmokeTest
@ -86,33 +100,23 @@ class BrowsingErrorPagesTest {
}
}
// Failing with network interruption, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1833874
// This tests the server ERROR_CONNECTION_REFUSED
@Test
fun connectionFailureErrorMessageTest() {
val url = "example.com"
fun connectionRefusedErrorMessageTest() {
val testUrl = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(url.toUri()) {
}.enterURLAndEnterToBrowser(testUrl.url) {
waitForPageToLoad()
verifyPageContent("Example Domain")
}
setNetworkEnabled(false)
browserScreen {
verifyPageContent(testUrl.content)
// Disconnecting the server
mockWebServer.shutdown()
}.openThreeDotMenu {
}.refreshPage {
waitForPageToLoad()
verifyConnectionErrorMessage()
}
setNetworkEnabled(true)
browserScreen {
}.openThreeDotMenu {
}.refreshPage {
waitForPageToLoad()
verifyPageContent("Example Domain")
}
}
@Test

@ -10,7 +10,6 @@ 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
@ -207,7 +206,6 @@ class CollectionTest {
// Test running on beta/release builds in CI:
// caution when making changes to it, so they don't block the builds
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1807289")
@SmokeTest
@Test
fun deleteCollectionTest() {

@ -0,0 +1,381 @@
/* 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 com.google.android.material.bottomsheet.BottomSheetBehavior
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.HomeActivityTestRule
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.verifySnackBarText
import org.mozilla.fenix.ui.robots.browserScreen
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 tabbed browsing
*
* Including:
* - Opening a tab
* - Opening a private tab
* - Verifying tab list
* - Closing all tabs
* - Close tab
* - Swipe to close tab (temporarily disabled)
* - Undo close tab
* - Close private tabs persistent notification
* - Empty tab tray state
* - Tab tray details
* - Shortcut context menu navigation
*/
class ComposeTabbedBrowsingTest {
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer
@get:Rule(order = 0)
val composeTestRule =
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()
}
@Test
fun openNewTabTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
mDevice.waitForIdle()
verifyTabCounter("1")
}.openComposeTabDrawer(composeTestRule) {
verifyNormalBrowsingButtonIsSelected()
verifyExistingOpenTabs("Test_Page_1")
closeTab()
}
homeScreen {
}.openComposeTabDrawer(composeTestRule) {
verifyNoOpenTabsInNormalBrowsing()
}.openNewTab {
}.submitQuery(defaultWebPage.url.toString()) {
mDevice.waitForIdle()
verifyTabCounter("1")
}.openComposeTabDrawer(composeTestRule) {
verifyNormalBrowsingButtonIsSelected()
verifyExistingOpenTabs("Test_Page_1")
}
}
@Test
fun openNewPrivateTabTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.togglePrivateBrowsingMode()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
mDevice.waitForIdle()
verifyTabCounter("1")
}.openComposeTabDrawer(composeTestRule) {
verifyPrivateTabsList()
verifyPrivateBrowsingButtonIsSelected()
}.toggleToNormalTabs {
verifyNoOpenTabsInNormalBrowsing()
}.toggleToPrivateTabs {
verifyPrivateTabsList()
}
}
@Test
fun closeAllTabsTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openComposeTabDrawer(composeTestRule) {
verifyNormalTabsList()
}.openThreeDotMenu {
verifyCloseAllTabsButton()
verifyShareAllTabsButton()
verifySelectTabsButton()
}.closeAllTabs {
verifyTabCounter("0")
}
// Repeat for Private Tabs
homeScreen {
}.togglePrivateBrowsingMode()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openComposeTabDrawer(composeTestRule) {
verifyPrivateTabsList()
}.openThreeDotMenu {
verifyCloseAllTabsButton()
}.closeAllTabs {
verifyTabCounter("0")
}
}
@Ignore("Being converted in: https://bugzilla.mozilla.org/show_bug.cgi?id=1832617")
@Test
fun closeTabTest() {
// val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
//
// navigationToolbar {
// }.enterURLAndEnterToBrowser(genericURL.url) {
// }.openTabDrawer {
// verifyExistingOpenTabs("Test_Page_1")
// closeTab()
// }
// homeScreen {
// verifyTabCounter("0")
// }.openNavigationToolbar {
// }.enterURLAndEnterToBrowser(genericURL.url) {
// }.openTabDrawer {
// verifyExistingOpenTabs("Test_Page_1")
// swipeTabRight("Test_Page_1")
// }
// homeScreen {
// verifyTabCounter("0")
// }.openNavigationToolbar {
// }.enterURLAndEnterToBrowser(genericURL.url) {
// }.openTabDrawer {
// verifyExistingOpenTabs("Test_Page_1")
// swipeTabLeft("Test_Page_1")
// }
// homeScreen {
// verifyTabCounter("0")
// }
}
@Test
fun verifyUndoSnackBarTest() {
// disabling these features because they interfere with the snackbar visibility
composeTestRule.activityRule.applySettingsExceptions {
it.isPocketEnabled = false
it.isRecentTabsFeatureEnabled = false
it.isRecentlyVisitedFeatureEnabled = false
}
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
}.openComposeTabDrawer(composeTestRule) {
verifyExistingOpenTabs("Test_Page_1")
closeTab()
verifySnackBarText("Tab closed")
clickSnackbarButton("UNDO")
}
browserScreen {
verifyTabCounter("1")
}.openComposeTabDrawer(composeTestRule) {
verifyExistingOpenTabs("Test_Page_1")
}
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1829838")
// Try converting in: https://bugzilla.mozilla.org/show_bug.cgi?id=1832609
@Test
fun closePrivateTabTest() {
// val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
//
// homeScreen { }.togglePrivateBrowsingMode()
// navigationToolbar {
// }.enterURLAndEnterToBrowser(genericURL.url) {
// }.openTabDrawer {
// verifyExistingOpenTabs("Test_Page_1")
// verifyCloseTabsButton("Test_Page_1")
// closeTab()
// }
// homeScreen {
// verifyTabCounter("0")
// }.openNavigationToolbar {
// }.enterURLAndEnterToBrowser(genericURL.url) {
// }.openTabDrawer {
// verifyExistingOpenTabs("Test_Page_1")
// swipeTabRight("Test_Page_1")
// }
// homeScreen {
// verifyTabCounter("0")
// }.openNavigationToolbar {
// }.enterURLAndEnterToBrowser(genericURL.url) {
// }.openTabDrawer {
// verifyExistingOpenTabs("Test_Page_1")
// swipeTabLeft("Test_Page_1")
// }
// homeScreen {
// verifyTabCounter("0")
// }
}
@Test
fun verifyPrivateTabUndoSnackBarTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen { }.togglePrivateBrowsingMode()
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
}.openComposeTabDrawer(composeTestRule) {
verifyExistingOpenTabs("Test_Page_1")
closeTab()
TestHelper.verifySnackBarText("Private tab closed")
TestHelper.clickSnackbarButton("UNDO")
}
browserScreen {
verifyTabCounter("1")
}.openComposeTabDrawer(composeTestRule) {
verifyExistingOpenTabs("Test_Page_1")
verifyPrivateBrowsingButtonIsSelected()
}
}
@Test
fun closePrivateTabsNotificationTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.togglePrivateBrowsingMode()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
mDevice.openNotification()
}
notificationShade {
verifyPrivateTabsNotification()
}.clickClosePrivateTabsNotification {
verifyHomeScreen()
}
}
@Test
fun verifyTabTrayNotShowingStateHalfExpanded() {
homeScreen {
}.openComposeTabDrawer(composeTestRule) {
verifyNoOpenTabsInNormalBrowsing()
// With no tabs opened the state should be STATE_COLLAPSED.
verifyTabsTrayBehaviorState(BottomSheetBehavior.STATE_COLLAPSED)
// Need to ensure the halfExpandedRatio is very small so that when in STATE_HALF_EXPANDED
// the tabTray will actually have a very small height (for a very short time) akin to being hidden.
verifyMinusculeHalfExpandedRatio()
}.clickTopBar {
}.waitForTabTrayBehaviorToIdle {
// Touching the topBar would normally advance the tabTray to the next state.
// We don't want that.
verifyTabsTrayBehaviorState(BottomSheetBehavior.STATE_COLLAPSED)
}.advanceToHalfExpandedState {
}.waitForTabTrayBehaviorToIdle {
// TabTray should not be displayed in STATE_HALF_EXPANDED.
// When advancing to this state it should immediately be hidden.
verifyTabTrayIsClosed()
}
}
@Test
fun verifyEmptyTabTray() {
homeScreen {
}.openComposeTabDrawer(composeTestRule) {
verifyNormalBrowsingButtonIsSelected()
verifyPrivateBrowsingButtonIsSelected(false)
verifySyncedTabsButtonIsSelected(false)
verifyNoOpenTabsInNormalBrowsing()
verifyFab()
verifyThreeDotButton()
}.openThreeDotMenu {
verifyTabSettingsButton()
verifyRecentlyClosedTabsButton()
}
}
@Test
fun verifyOpenTabDetails() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openComposeTabDrawer(composeTestRule) {
verifyNormalBrowsingButtonIsSelected()
verifyPrivateBrowsingButtonIsSelected(isSelected = false)
verifySyncedTabsButtonIsSelected(isSelected = false)
verifyThreeDotButton()
verifyNormalTabCounter()
verifyNormalTabsList()
verifyFab()
verifyTabThumbnail()
verifyExistingOpenTabs(defaultWebPage.title)
verifyTabCloseButton(defaultWebPage.title)
}.openTab(defaultWebPage.title) {
verifyUrl(defaultWebPage.url.toString())
verifyTabCounter("1")
}
}
@Test
fun verifyContextMenuShortcuts() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openTabButtonShortcutsMenu {
verifyTabButtonShortcutMenuItems()
}.closeTabFromShortcutsMenu {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openTabButtonShortcutsMenu {
}.openNewPrivateTabFromShortcutsMenu {
verifyKeyboardVisible()
verifyFocusedNavigationToolbar()
// dismiss search dialog
homeScreen { }.pressBack()
verifyCommonMythsLink()
verifyNavigationToolbar()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openTabButtonShortcutsMenu {
}.openTabFromShortcutsMenu {
verifyKeyboardVisible()
verifyFocusedNavigationToolbar()
// dismiss search dialog
homeScreen { }.pressBack()
verifyHomeWordmark()
verifyNavigationToolbar()
}
}
}

@ -15,10 +15,11 @@ class CookieBannerReductionTest {
@get:Rule
val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
// Bug causing flakiness https://bugzilla.mozilla.org/show_bug.cgi?id=1807440
@SmokeTest
@Test
fun verifyCookieBannerReductionTest() {
val webSite = "voetbal24.be"
val webSite = "startsiden.no"
homeScreen {
}.openNavigationToolbar {
@ -27,13 +28,13 @@ class CookieBannerReductionTest {
verifyCookieBannerExists(exists = true)
}.openThreeDotMenu {
}.openSettings {
verifySettingsOptionSummary("Cookie Banner Reduction", "Off")
verifySettingsOptionSummary("Cookie banner reduction", "Off")
}.openCookieBannerReductionSubMenu {
verifyCookieBannerView(isCookieBannerReductionChecked = false)
clickCookieBannerReductionToggle()
verifyCheckedCookieBannerReductionToggle(isCookieBannerReductionChecked = true)
}.goBack {
verifySettingsOptionSummary("Cookie Banner Reduction", "On")
verifySettingsOptionSummary("Cookie banner reduction", "On")
}
exitMenu()
@ -62,10 +63,11 @@ class CookieBannerReductionTest {
}
}
// Bug causing flakiness https://bugzilla.mozilla.org/show_bug.cgi?id=1807440
@SmokeTest
@Test
fun verifyCookieBannerReductionInPrivateBrowsingTest() {
val webSite = "voetbal24.be"
val webSite = "startsiden.no"
homeScreen {
}.togglePrivateBrowsingMode()
@ -76,13 +78,13 @@ class CookieBannerReductionTest {
verifyCookieBannerExists(exists = true)
}.openThreeDotMenu {
}.openSettings {
verifySettingsOptionSummary("Cookie Banner Reduction", "Off")
verifySettingsOptionSummary("Cookie banner reduction", "Off")
}.openCookieBannerReductionSubMenu {
verifyCookieBannerView(isCookieBannerReductionChecked = false)
clickCookieBannerReductionToggle()
verifyCheckedCookieBannerReductionToggle(isCookieBannerReductionChecked = true)
}.goBack {
verifySettingsOptionSummary("Cookie Banner Reduction", "On")
verifySettingsOptionSummary("Cookie banner reduction", "On")
}
exitMenu()
@ -95,7 +97,7 @@ class CookieBannerReductionTest {
homeScreen {
}.openTabDrawer {
}.openTab("Voetbal24") {
}.openTab("Startsiden.no") {
verifyCookieBannerExists(exists = false)
}.openThreeDotMenu {
}.openSettings {

@ -77,7 +77,6 @@ class CrashReportingTest {
}
}
@Ignore("Failure: https://bugzilla.mozilla.org/show_bug.cgi?id=1812075")
@SmokeTest
@Test
fun useAppWhileTabIsCrashedTest() {

@ -9,6 +9,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.IntentReceiverActivity
@ -174,6 +175,7 @@ class CustomTabsTest {
}
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1807289")
@SmokeTest
@Test
fun customTabDownloadTest() {

@ -8,7 +8,6 @@ import androidx.core.net.toUri
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
@ -185,7 +184,6 @@ class DownloadTest {
}
}
@Ignore("failing, see https://bugzilla.mozilla.org/show_bug.cgi?id=1821024")
@SmokeTest
@Test
fun openDownloadedFileTest() {

@ -101,6 +101,11 @@ class HomeScreenTest {
@Test
fun verifyJumpBackInSectionTest() {
activityTestRule.activityRule.applySettingsExceptions {
it.isRecentlyVisitedFeatureEnabled = false
it.isPocketEnabled = false
}
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 4)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)

@ -209,8 +209,6 @@ class MainMenuTest {
}.openThreeDotMenu {
}.clickShareButton {
verifyShareTabLayout()
verifySendToDeviceTitle()
verifyShareALinkTitle()
}
}

@ -10,7 +10,6 @@ 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
@ -234,13 +233,13 @@ class NavigationToolbarTest {
}
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1829104")
@Test
fun verifyInsecurePageSecuritySubMenuTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
waitForPageToLoad()
}.openSiteSecuritySheet {
verifyQuickActionSheet(defaultWebPage.url.toString(), false)
openSecureConnectionSubMenu(false)

@ -9,6 +9,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import io.mockk.mockk
import mozilla.components.concept.sync.AuthType
import mozilla.components.service.fxa.FirefoxAccount
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assert.assertTrue
@ -65,7 +66,8 @@ class NimbusEventTest {
@Test
fun telemetryAccountObserverTest() {
val observer = TelemetryAccountObserver(appContext)
observer.onAuthenticated(mockk(), AuthType.Signin)
// replacing interface mock with implementation mock.
observer.onAuthenticated(mockk<FirefoxAccount>(), AuthType.Signin)
Experimentation.withHelper {
assertTrue(evalJexl("'sync_auth.sign_in'|eventSum('Days', 28, 0) > 0"))

@ -113,6 +113,7 @@ class OnboardingTest {
}
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1807268")
@Test
fun dismissOnboardingUsingHelpTest() {
homeScreen {

@ -56,7 +56,6 @@ class PwaTest {
}
}
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/28212")
@SmokeTest
@Test
fun emailLinkPWATest() {
@ -69,6 +68,7 @@ class PwaTest {
clickAddAutomaticallyButton()
}.openHomeScreenShortcut(shortcutTitle) {
clickPageObject(itemContainingText("Email link"))
clickPageObject(itemWithResIdAndText("android:id/button1", "OPEN"))
assertNativeAppOpens(GMAIL_APP, emailLink)
}
}
@ -109,6 +109,7 @@ class PwaTest {
}
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1807273")
@SmokeTest
@Test
fun saveLoginsInPWATest() {

@ -93,7 +93,6 @@ class SearchTest {
}
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1821993")
@SmokeTest
@Test
fun scanButtonDenyPermissionTest() {
@ -284,7 +283,6 @@ class SearchTest {
}
}
@Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
@SmokeTest
@Test
fun noSearchGroupFromPrivateBrowsingTest() {

@ -4,7 +4,6 @@
package org.mozilla.fenix.ui
import android.view.View
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
@ -19,7 +18,8 @@ import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper.getEnhancedTrackingProtectionAsset
import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
import org.mozilla.fenix.helpers.TestHelper.registerAndCleanupIdlingResources
import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource
import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
import org.mozilla.fenix.helpers.TestHelper.waitUntilSnackbarGone
import org.mozilla.fenix.ui.robots.addonsMenu
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
@ -102,13 +102,12 @@ class SettingsAddonsTest {
verifyAddonInstallCompleted(addonName, activityTestRule)
closeAddonInstallCompletePrompt()
}.openDetailedMenuForAddon(addonName) {
registerAndCleanupIdlingResources(
ViewVisibilityIdlingResource(
activityTestRule.activity.findViewById(R.id.addon_container),
View.VISIBLE,
),
) {}
}.removeAddon {
}.removeAddon(activityTestRule) {
verifySnackBarText("Successfully uninstalled $addonName")
waitUntilSnackbarGone()
}.goBack {
}.openThreeDotMenu {
}.openAddonsManagerMenu {
verifyAddonCanBeInstalled(addonName)
}
}

@ -6,7 +6,6 @@ package org.mozilla.fenix.ui
import androidx.core.net.toUri
import androidx.test.espresso.Espresso.pressBack
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
@ -121,7 +120,6 @@ class SettingsHTTPSOnlyModeTest {
}
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1826500")
@Test
fun httpsOnlyModeEnabledOnlyInPrivateBrowsingTest() {
homeScreen {

@ -54,7 +54,7 @@ class SettingsPrivacyTest {
verifyHTTPSOnlyModeButton()
verifySettingsOptionSummary("HTTPS-Only Mode", "Off")
verifyCookieBannerReductionButton()
verifySettingsOptionSummary("Cookie Banner Reduction", "Off")
verifySettingsOptionSummary("Cookie banner reduction", "Off")
verifyEnhancedTrackingProtectionButton()
verifySettingsOptionSummary("Enhanced Tracking Protection", "Standard")
verifySitePermissionsButton()

@ -283,11 +283,6 @@ class SettingsSearchTest {
verifyAllowSuggestionsInPrivateModeDialog()
denySuggestionsInPrivateMode()
verifyNoSuggestionsAreDisplayed(activityTestRule, "mozilla firefox")
}.dismissSearchBar {
togglePrivateBrowsingModeOnOff()
}.openSearch {
typeSearch("mozilla")
verifySearchEngineSuggestionResults(activityTestRule, "mozilla firefox")
}
}

@ -108,7 +108,6 @@ class SettingsSitePermissionsTest {
}
}
@Ignore("Failing, see https://bugzilla.mozilla.org/show_bug.cgi?id=1827599")
@SmokeTest
@Test
fun verifyAutoplayBlockAudioOnlySettingTest() {
@ -174,7 +173,6 @@ class SettingsSitePermissionsTest {
}
}
@Ignore("Failing, see https://bugzilla.mozilla.org/show_bug.cgi?id=1827599")
@Test
fun verifyAutoplayAllowAudioVideoSettingTest() {
val genericPage = getGenericAsset(mockWebServer, 1)
@ -237,7 +235,6 @@ class SettingsSitePermissionsTest {
}
}
@Ignore("Failing, see https://bugzilla.mozilla.org/show_bug.cgi?id=1827599")
@Test
fun verifyAutoplayBlockAudioAndVideoSettingTest() {
val videoTestPage = getVideoPageAsset(mockWebServer)
@ -268,7 +265,6 @@ class SettingsSitePermissionsTest {
}
}
@Ignore("Failing, see https://bugzilla.mozilla.org/show_bug.cgi?id=1827599")
@Test
fun verifyAutoplayBlockAudioAndVideoSettingOnMutedVideoTest() {
val mutedVideoTestPage = getMutedVideoPageAsset(mockWebServer)

@ -371,10 +371,10 @@ class SmokeTest {
navigationToolbar {
verifyReaderViewDetected(true)
toggleReaderView()
mDevice.waitForIdle()
}
browserScreen {
waitForPageToLoad()
verifyPageContent(estimatedReadingTime)
}.openThreeDotMenu {
verifyReaderViewAppearance(true)

@ -10,7 +10,6 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
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
@ -198,7 +197,6 @@ class TabbedBrowsingTest {
}
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1829838")
@Test
fun closePrivateTabTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)

@ -5,7 +5,6 @@ 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
@ -139,7 +138,6 @@ class TextSelectionTest {
}
}
@Ignore("Failing, see https://bugzilla.mozilla.org/show_bug.cgi?id=1828663")
@SmokeTest
@Test
fun selectAllAndCopyPDFTextTest() {
@ -160,9 +158,7 @@ class TextSelectionTest {
clickClearButton()
longClickToolbar()
clickPasteText()
// With Select all, white spaces are copied
// Potential bug https://bugzilla.mozilla.org/show_bug.cgi?id=1821310
verifyTypedToolbarText(" Washington Crossing the Delaware Wikipedia link ")
verifyTypedToolbarText("Washington Crossing the Delaware Wikipedia link")
}
}

@ -12,6 +12,7 @@ import androidx.test.espresso.action.ViewActions.clearText
import androidx.test.espresso.action.ViewActions.longClick
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.PositionAssertions
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.RootMatchers
@ -31,10 +32,14 @@ import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.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.Constants.RETRY_COUNT
import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists
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.itemContainingText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
@ -242,6 +247,72 @@ class BookmarksRobot {
fun clickDeleteInEditModeButton() = deleteInEditModeButton().click()
fun clickSearchButton() = itemWithResId("$packageName:id/bookmark_search").click()
fun verifyBookmarksSearchBarPosition(defaultPosition: Boolean) {
onView(withId(R.id.toolbar))
.check(
if (defaultPosition) {
PositionAssertions.isCompletelyBelow(withId(R.id.pill_wrapper_divider))
} else {
PositionAssertions.isCompletelyAbove(withId(R.id.pill_wrapper_divider))
},
)
}
fun clickOutsideTheSearchBar() {
itemWithResId("$packageName:id/search_wrapper").click()
itemWithResId("$packageName:id/mozac_browser_toolbar_edit_url_view")
.waitUntilGone(waitingTime)
}
fun dismissBookmarksSearchBarUsingBackButton() {
for (i in 1..RETRY_COUNT) {
try {
mDevice.pressBack()
assertTrue(
itemWithResId("$packageName:id/mozac_browser_toolbar_edit_url_view")
.waitUntilGone(waitingTime),
)
break
} catch (e: AssertionError) {
if (i == RETRY_COUNT) {
throw e
}
}
}
}
fun verifyBookmarksSearchBar(exists: Boolean) {
assertItemWithResIdExists(
itemWithResId("$packageName:id/toolbar"),
itemWithResId("$packageName:id/mozac_browser_toolbar_edit_icon"),
exists = exists,
)
assertItemWithResIdAndTextExists(
itemWithResId("$packageName:id/mozac_browser_toolbar_edit_url_view"),
itemContainingText(getStringResource(R.string.bookmark_search)),
exists = exists,
)
assertItemWithDescriptionExists(
itemWithDescription(getStringResource(R.string.voice_search_content_description)),
exists = exists,
)
}
fun searchBookmarkedItem(bookmarkedItem: String) {
itemWithResId("$packageName:id/mozac_browser_toolbar_edit_url_view").also {
it.waitForExists(waitingTime)
it.setText(bookmarkedItem)
}
mDevice.waitForWindowUpdate(packageName, waitingTimeShort)
}
fun verifySearchedBookmarkExists(bookmarkUrl: String, exists: Boolean = true) =
assertItemContainingTextExists(itemContainingText(bookmarkUrl), exists = exists)
fun dismissBookmarksSearchBar() = mDevice.pressBack()
class Transition {
fun closeMenu(interact: HomeScreenRobot.() -> Unit): Transition {
closeButton().click()
@ -279,6 +350,13 @@ class BookmarksRobot {
return HomeScreenRobot.Transition()
}
fun goBackToBrowserScreen(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
goBackButton().click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun closeEditBookmarkSection(interact: BookmarksRobot.() -> Unit): BookmarksRobot.Transition {
goBackButton().click()
@ -385,7 +463,7 @@ private fun assertBookmarkFolderIsNotCreated(title: String) {
mDevice.findObject(
UiSelector()
.textContains(title),
).waitForExists(waitingTime),
).waitForExists(waitingTimeShort),
)
}
@ -417,7 +495,7 @@ private fun assertBookmarkIsDeleted(expectedTitle: String) {
UiSelector()
.resourceId("$packageName:id/title")
.textContains(expectedTitle),
).waitForExists(waitingTime),
).waitForExists(waitingTimeShort),
)
}
private fun assertUndoDeleteSnackBarButton() =

@ -11,6 +11,7 @@ import android.net.Uri
import android.os.SystemClock
import android.util.Log
import android.widget.TimePicker
import androidx.compose.ui.test.onNodeWithTag
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
@ -38,6 +39,7 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION
import org.mozilla.fenix.helpers.Constants.RETRY_COUNT
import org.mozilla.fenix.helpers.HomeActivityComposeTestRule
import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithDescriptionExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdAndTextExists
@ -51,11 +53,13 @@ import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
import org.mozilla.fenix.helpers.SessionLoadedIdlingResource
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.waitForObjects
import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.tabstray.TabsTrayTestTag
import java.time.LocalDate
class BrowserRobot {
@ -640,7 +644,7 @@ class BrowserRobot {
mDevice.findObject(
UiSelector()
.text("Selected date is: $currentDate"),
).waitForExists(waitingTime),
).waitForExists(waitingTimeShort),
)
}
@ -743,7 +747,7 @@ class BrowserRobot {
mDevice.findObject(
UiSelector()
.text("Selected date is: $hour:$minute"),
).waitForExists(waitingTime),
).waitForExists(waitingTimeShort),
)
}
@ -752,7 +756,7 @@ class BrowserRobot {
mDevice.findObject(
UiSelector()
.text("Selected date is: $hexValue"),
).waitForExists(waitingTime),
).waitForExists(waitingTimeShort),
)
}
@ -918,6 +922,37 @@ class BrowserRobot {
return TabDrawerRobot.Transition()
}
fun openComposeTabDrawer(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
for (i in 1..RETRY_COUNT) {
try {
mDevice.waitForObjects(
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/mozac_browser_toolbar_browser_actions"),
),
waitingTime,
)
tabsCounter().click()
composeTestRule.onNodeWithTag(TabsTrayTestTag.tabsTray).assertExists()
break
} catch (e: AssertionError) {
if (i == RETRY_COUNT) {
throw e
} else {
mDevice.waitForIdle()
}
}
}
composeTestRule.onNodeWithTag(TabsTrayTestTag.fab).assertExists()
ComposeTabDrawerRobot(composeTestRule).interact()
return ComposeTabDrawerRobot.Transition(composeTestRule)
}
fun openTabButtonShortcutsMenu(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.desc("Tabs")))
tabsCounter().click(LONG_CLICK_DURATION)
@ -1092,7 +1127,7 @@ fun homeScreenButton() = onView(withContentDescription(R.string.browser_toolbar_
private fun threeDotButton() = onView(withContentDescription("Menu"))
private fun tabsCounter() =
mDevice.findObject(By.res("$packageName:id/mozac_browser_toolbar_browser_actions"))
mDevice.findObject(By.res("$packageName:id/counter_root"))
private val progressBar =
itemWithResId("$packageName:id/mozac_browser_toolbar_progress")
@ -1194,7 +1229,7 @@ private val currentDate = LocalDate.now()
private val currentDay = currentDate.dayOfMonth
private val currentMonth = currentDate.month
private val currentYear = currentDate.year
private val cookieBanner = itemWithResId("CybotCookiebotDialog")
private val cookieBanner = itemWithResId("startsiden-gdpr-disclaimer")
private val totalCookieProtectionHintMessage =
itemContainingText(getStringResource(R.string.tcp_cfr_message))
private val totalCookieProtectionHintLearnMoreLink =

@ -0,0 +1,377 @@
/* 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("TooManyFunctions")
package org.mozilla.fenix.ui.robots
import android.view.View
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsNotSelected
import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.hasAnyChild
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.junit4.ComposeTestRule
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.test.espresso.Espresso
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
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 org.hamcrest.Matcher
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.HomeActivityComposeTestRule
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.clickAtLocationInView
import org.mozilla.fenix.helpers.idlingresource.BottomSheetBehaviorStateIdlingResource
import org.mozilla.fenix.helpers.matchers.BottomSheetBehaviorHalfExpandedMaxRatioMatcher
import org.mozilla.fenix.helpers.matchers.BottomSheetBehaviorStateMatcher
import org.mozilla.fenix.tabstray.TabsTrayTestTag
/**
* Implementation of Robot Pattern for the Tabs Tray.
*/
class ComposeTabDrawerRobot(private val composeTestRule: HomeActivityComposeTestRule) {
fun verifyNormalBrowsingButtonIsSelected(isSelected: Boolean = true) {
if (isSelected) {
composeTestRule.normalBrowsingButton().assertIsSelected()
} else {
composeTestRule.normalBrowsingButton().assertIsNotSelected()
}
}
fun verifyPrivateBrowsingButtonIsSelected(isSelected: Boolean = true) {
if (isSelected) {
composeTestRule.privateBrowsingButton().assertIsSelected()
} else {
composeTestRule.privateBrowsingButton().assertIsNotSelected()
}
}
fun verifySyncedTabsButtonIsSelected(isSelected: Boolean = true) {
if (isSelected) {
composeTestRule.syncedTabsButton().assertIsSelected()
} else {
composeTestRule.syncedTabsButton().assertIsNotSelected()
}
}
fun verifyExistingOpenTabs(vararg titles: String) {
titles.forEach { title ->
composeTestRule.tabItem(title).assertExists()
}
}
fun verifyNormalTabsList() {
composeTestRule.normalTabsList().assertExists()
}
fun verifyPrivateTabsList() {
composeTestRule.privateTabsList().assertExists()
}
fun verifySyncedTabsList() {
composeTestRule.syncedTabsList().assertExists()
}
fun verifyNoOpenTabsInNormalBrowsing() {
composeTestRule.emptyNormalTabsList().assertExists()
}
fun verifyNoOpenTabsInPrivateBrowsing() {
composeTestRule.emptyPrivateTabsList().assertExists()
}
fun verifyAccountSettingsButton() {
composeTestRule.dropdownMenuItemAccountSettings().assertExists()
}
fun verifyCloseAllTabsButton() {
composeTestRule.dropdownMenuItemCloseAllTabs().assertExists()
}
fun verifySelectTabsButton() {
composeTestRule.dropdownMenuItemSelectTabs().assertExists()
}
fun verifyShareAllTabsButton() {
composeTestRule.dropdownMenuItemShareAllTabs().assertExists()
}
fun verifyRecentlyClosedTabsButton() {
composeTestRule.dropdownMenuItemRecentlyClosedTabs().assertExists()
}
fun verifyTabSettingsButton() {
composeTestRule.dropdownMenuItemTabSettings().assertExists()
}
fun verifyThreeDotButton() {
composeTestRule.threeDotButton().assertExists()
}
fun verifyFab() {
composeTestRule.tabsTrayFab().assertExists()
}
fun verifyNormalTabCounter() {
composeTestRule.normalTabsCounter().assertExists()
}
/**
* Verifies a tab's thumbnail when there is only one tab open.
*/
fun verifyTabThumbnail() {
composeTestRule.tabThumbnail().assertExists()
}
/**
* Verifies a tab with [title] has a close button.
*/
fun verifyTabCloseButton(title: String) {
composeTestRule.tabItem(title).assert(
hasAnyChild(
hasTestTag(TabsTrayTestTag.tabItemClose),
),
)
}
fun verifyTabsTrayBehaviorState(expectedState: Int) {
tabsTrayView().check(ViewAssertions.matches(BottomSheetBehaviorStateMatcher(expectedState)))
}
fun verifyMinusculeHalfExpandedRatio() {
tabsTrayView().check(ViewAssertions.matches(BottomSheetBehaviorHalfExpandedMaxRatioMatcher(0.001f)))
}
fun verifyTabTrayIsClosed() {
composeTestRule.tabsTray().assertDoesNotExist()
}
/**
* Closes a tab when there is only one tab open.
*/
fun closeTab() {
composeTestRule.closeTabButton().performClick()
}
class Transition(private val composeTestRule: HomeActivityComposeTestRule) {
fun openNewTab(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
mDevice.waitForIdle()
composeTestRule.tabsTrayFab().performClick()
SearchRobot().interact()
return SearchRobot.Transition()
}
fun toggleToNormalTabs(interact: ComposeTabDrawerRobot.() -> Unit): Transition {
composeTestRule.normalBrowsingButton().performClick()
ComposeTabDrawerRobot(composeTestRule).interact()
return Transition(composeTestRule)
}
fun toggleToPrivateTabs(interact: ComposeTabDrawerRobot.() -> Unit): Transition {
composeTestRule.privateBrowsingButton().performClick()
ComposeTabDrawerRobot(composeTestRule).interact()
return Transition(composeTestRule)
}
fun openThreeDotMenu(interact: ComposeTabDrawerRobot.() -> Unit): Transition {
composeTestRule.threeDotButton().performClick()
ComposeTabDrawerRobot(composeTestRule).interact()
return Transition(composeTestRule)
}
fun closeAllTabs(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
composeTestRule.dropdownMenuItemCloseAllTabs().performClick()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun openTab(title: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
composeTestRule.tabItem(title)
.performScrollTo()
.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.
Espresso.onView(ViewMatchers.withId(R.id.topBar)).clickAtLocationInView(GeneralLocation.TOP_RIGHT)
ComposeTabDrawerRobot(composeTestRule).interact()
return Transition(composeTestRule)
}
fun waitForTabTrayBehaviorToIdle(interact: ComposeTabDrawerRobot.() -> Unit): Transition {
// Need to get the behavior of tab_wrapper and wait for that to idle.
var behavior: BottomSheetBehavior<*>? = null
// Null check here since it's possible that the view is already animated away from the screen.
tabsTrayView()?.perform(
object : ViewAction {
override fun getDescription(): String {
return "Postpone actions to after the BottomSheetBehavior has settled"
}
override fun getConstraints(): Matcher<View> {
return ViewMatchers.isAssignableFrom(View::class.java)
}
override fun perform(uiController: UiController?, view: View?) {
behavior = BottomSheetBehavior.from(view!!)
}
},
)
behavior?.let {
runWithIdleRes(BottomSheetBehaviorStateIdlingResource(it)) {
ComposeTabDrawerRobot(composeTestRule).interact()
}
}
return Transition(composeTestRule)
}
fun advanceToHalfExpandedState(interact: ComposeTabDrawerRobot.() -> Unit): Transition {
tabsTrayView().perform(
object : ViewAction {
override fun getDescription(): String {
return "Advance a BottomSheetBehavior to STATE_HALF_EXPANDED"
}
override fun getConstraints(): Matcher<View> {
return ViewMatchers.isAssignableFrom(View::class.java)
}
override fun perform(uiController: UiController?, view: View?) {
val behavior = BottomSheetBehavior.from(view!!)
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
}
},
)
ComposeTabDrawerRobot(composeTestRule).interact()
return Transition(composeTestRule)
}
}
}
/**
* Obtains the root [View] that wraps the Tabs Tray.
*/
private fun tabsTrayView() = Espresso.onView(ViewMatchers.withId(R.id.tabs_tray_root))
/**
* Obtains the root Tabs Tray.
*/
private fun ComposeTestRule.tabsTray() = onNodeWithTag(TabsTrayTestTag.tabsTray)
/**
* Obtains the Tabs Tray FAB.
*/
private fun ComposeTestRule.tabsTrayFab() = onNodeWithTag(TabsTrayTestTag.fab)
/**
* Obtains the normal browsing page button of the Tabs Tray banner.
*/
private fun ComposeTestRule.normalBrowsingButton() = onNodeWithTag(TabsTrayTestTag.normalTabsPageButton)
/**
* Obtains the private browsing page button of the Tabs Tray banner.
*/
private fun ComposeTestRule.privateBrowsingButton() = onNodeWithTag(TabsTrayTestTag.privateTabsPageButton)
/**
* Obtains the synced tabs page button of the Tabs Tray banner.
*/
private fun ComposeTestRule.syncedTabsButton() = onNodeWithTag(TabsTrayTestTag.syncedTabsPageButton)
/**
* Obtains the normal tabs list.
*/
private fun ComposeTestRule.normalTabsList() = onNodeWithTag(TabsTrayTestTag.normalTabsList)
/**
* Obtains the private tabs list.
*/
private fun ComposeTestRule.privateTabsList() = onNodeWithTag(TabsTrayTestTag.privateTabsList)
/**
* Obtains the synced tabs list.
*/
private fun ComposeTestRule.syncedTabsList() = onNodeWithTag(TabsTrayTestTag.syncedTabsList)
/**
* Obtains the empty normal tabs list.
*/
private fun ComposeTestRule.emptyNormalTabsList() = onNodeWithTag(TabsTrayTestTag.emptyNormalTabsList)
/**
* Obtains the empty private tabs list.
*/
private fun ComposeTestRule.emptyPrivateTabsList() = onNodeWithTag(TabsTrayTestTag.emptyPrivateTabsList)
/**
* Obtains the tab with the provided [title]
*/
private fun ComposeTestRule.tabItem(title: String) = onNodeWithText(title)
/**
* Obtains an open tab's close button when there's only one tab open.
*/
private fun ComposeTestRule.closeTabButton() = onNodeWithTag(TabsTrayTestTag.tabItemClose)
/**
* Obtains an open tab's thumbnail when there's only one tab open.
*/
private fun ComposeTestRule.tabThumbnail() = onNodeWithTag(TabsTrayTestTag.tabItemThumbnail)
/**
* Obtains the three dot button in the Tabs Tray banner.
*/
private fun ComposeTestRule.threeDotButton() = onNodeWithTag(TabsTrayTestTag.threeDotButton)
/**
* Obtains the dropdown menu item to access account settings.
*/
private fun ComposeTestRule.dropdownMenuItemAccountSettings() = onNodeWithTag(TabsTrayTestTag.accountSettings)
/**
* Obtains the dropdown menu item to close all tabs.
*/
private fun ComposeTestRule.dropdownMenuItemCloseAllTabs() = onNodeWithTag(TabsTrayTestTag.closeAllTabs)
/**
* Obtains the dropdown menu item to access recently closed tabs.
*/
private fun ComposeTestRule.dropdownMenuItemRecentlyClosedTabs() = onNodeWithTag(TabsTrayTestTag.recentlyClosedTabs)
/**
* Obtains the dropdown menu item to select tabs.
*/
private fun ComposeTestRule.dropdownMenuItemSelectTabs() = onNodeWithTag(TabsTrayTestTag.selectTabs)
/**
* Obtains the dropdown menu item to share all tabs.
*/
private fun ComposeTestRule.dropdownMenuItemShareAllTabs() = onNodeWithTag(TabsTrayTestTag.shareAllTabs)
/**
* Obtains the dropdown menu item to access tab settings.
*/
private fun ComposeTestRule.dropdownMenuItemTabSettings() = onNodeWithTag(TabsTrayTestTag.tabSettings)
/**
* Obtains the normal tabs counter.
*/
private fun ComposeTestRule.normalTabsCounter() = onNodeWithTag(TabsTrayTestTag.normalTabsCounter)

@ -24,8 +24,8 @@ 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.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
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
@ -107,13 +107,13 @@ class HistoryRobot {
assertTrue(
mDevice.findObject(UiSelector().text(searchTerm))
.getFromParent(UiSelector().text("$groupSize sites"))
.waitForExists(TestAssetHelper.waitingTimeShort),
.waitForExists(waitingTimeShort),
)
} else {
assertFalse(
mDevice.findObject(UiSelector().text(searchTerm))
.getFromParent(UiSelector().text("$groupSize sites"))
.waitForExists(TestAssetHelper.waitingTimeShort),
.waitForExists(waitingTimeShort),
)
}
}
@ -175,7 +175,7 @@ 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(waitingTime))
assertFalse(mDevice.findObject(UiSelector().textContains(item)).waitForExists(waitingTimeShort))
}
}
@ -213,7 +213,7 @@ private fun deleteHistoryPromptSummary() =
mDevice
.findObject(
UiSelector()
.textContains(getStringResource(R.string.delete_history_prompt_body))
.textContains(getStringResource(R.string.delete_history_prompt_body_2))
.resourceId("$packageName:id/body"),
)

@ -57,6 +57,7 @@ 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
import org.mozilla.fenix.helpers.MatcherHelper.assertCheckedItemWithResIdExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithDescriptionExists
@ -80,6 +81,7 @@ 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
/**
@ -181,7 +183,7 @@ class HomeScreenRobot {
itemWithResId("$packageName:id/tracking_protection_strict_default").click()
fun verifyPrivacyNoticeCard() {
scrollToElementByText(getStringResource(R.string.onboarding_privacy_notice_header_1))
scrollToElementByText(getStringResource(R.string.onboarding_privacy_notice_read_button))
assertItemContainingTextExists(privacyNoticeHeader, privacyNoticeDescription)
assertItemWithResIdExists(privacyNoticeButton)
}
@ -290,7 +292,7 @@ class HomeScreenRobot {
).getChild(
UiSelector()
.textContains(sponsoredShortcutTitle),
).waitForExists(waitingTime),
).waitForExists(waitingTimeShort),
)
fun verifyNotExistingSponsoredTopSitesList() = assertSponsoredTopSitesNotDisplayed()
fun verifyExistingTopSitesTabs(title: String) = assertExistingTopSitesTabs(title)
@ -380,7 +382,7 @@ class HomeScreenRobot {
.textContains(
getStringResource(R.string.pocket_stories_header_1),
),
).waitForExists(waitingTime),
).waitForExists(waitingTimeShort),
)
}
}
@ -437,7 +439,7 @@ class HomeScreenRobot {
.textContains(
getStringResource(R.string.pocket_stories_categories_header),
),
).waitForExists(waitingTime),
).waitForExists(waitingTimeShort),
)
}
}
@ -481,7 +483,7 @@ class HomeScreenRobot {
mDevice.findObject(
UiSelector()
.textContains("Customize homepage"),
).waitForExists(waitingTime),
).waitForExists(waitingTimeShort),
)
}
}
@ -550,6 +552,15 @@ class HomeScreenRobot {
return TabDrawerRobot.Transition()
}
fun openComposeTabDrawer(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
mDevice.waitForIdle(waitingTime)
onView(withId(R.id.tab_button)).click()
composeTestRule.onNodeWithTag(TabsTrayTestTag.tabsTray).assertExists()
ComposeTabDrawerRobot(composeTestRule).interact()
return ComposeTabDrawerRobot.Transition(composeTestRule)
}
fun openThreeDotMenu(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition {
// Issue: https://github.com/mozilla-mobile/fenix/issues/21578
try {
@ -570,6 +581,7 @@ class HomeScreenRobot {
fun openSearch(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
navigationToolbar.waitForExists(waitingTime)
navigationToolbar.click()
mDevice.waitForIdle()
SearchRobot().interact()
return SearchRobot.Transition()
@ -979,7 +991,7 @@ private fun assertNotExistingTopSitesList(title: String) {
UiSelector()
.resourceId("$packageName:id/top_site_title")
.textContains(title),
).waitForExists(waitingTime),
).waitForExists(waitingTimeShort),
)
}
@ -989,7 +1001,7 @@ private fun assertSponsoredTopSitesNotDisplayed() {
UiSelector()
.resourceId("$packageName:id/top_site_subtitle")
.textContains(getStringResource(R.string.top_sites_sponsored_label)),
).waitForExists(waitingTime),
).waitForExists(waitingTimeShort),
)
}
@ -1023,7 +1035,7 @@ private fun assertJumpBackInShowAllButton() =
private fun assertRecentlyVisitedSectionIsDisplayed() = assertTrue(recentlyVisitedSection().waitForExists(waitingTime))
private fun assertRecentlyVisitedSectionIsNotDisplayed() = assertFalse(recentlyVisitedSection().waitForExists(waitingTime))
private fun assertRecentlyVisitedSectionIsNotDisplayed() = assertFalse(recentlyVisitedSection().waitForExists(waitingTimeShort))
private fun assertRecentBookmarksSectionIsDisplayed() =
assertTrue(recentBookmarksSection().waitForExists(waitingTime))
@ -1033,7 +1045,7 @@ private fun assertRecentBookmarksSectionIsNotDisplayed() =
private fun assertPocketSectionIsDisplayed() = assertTrue(pocketSection().waitForExists(waitingTime))
private fun assertPocketSectionIsNotDisplayed() = assertFalse(pocketSection().waitForExists(waitingTime))
private fun assertPocketSectionIsNotDisplayed() = assertFalse(pocketSection().waitForExists(waitingTimeShort))
private fun saveTabsToCollectionButton() = onView(withId(R.id.add_tabs_to_collections_button))

@ -83,12 +83,12 @@ class NavigationToolbarRobot {
false -> {
assertFalse(
mDevice.findObject(UiSelector().resourceId("$packageName:id/fill_link_from_clipboard"))
.waitForExists(waitingTime),
.waitForExists(waitingTimeShort),
)
assertFalse(
mDevice.findObject(UiSelector().resourceId("$packageName:id/clipboard_url").text(link))
.waitForExists(waitingTime),
.waitForExists(waitingTimeShort),
)
}
}

@ -6,7 +6,6 @@
package org.mozilla.fenix.ui.robots
import android.os.Build
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.assertIsDisplayed
@ -39,11 +38,13 @@ import org.mozilla.fenix.helpers.Constants
import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION
import org.mozilla.fenix.helpers.Constants.RETRY_COUNT
import org.mozilla.fenix.helpers.Constants.SPEECH_RECOGNITION
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
import org.mozilla.fenix.helpers.TestHelper.getStringResource
import org.mozilla.fenix.helpers.TestHelper.grantSystemPermission
import org.mozilla.fenix.helpers.TestHelper.isPackageInstalled
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
@ -62,32 +63,17 @@ class SearchRobot {
if (enabled) {
assertTrue(voiceSearchButton.waitForExists(waitingTime))
} else {
assertFalse(voiceSearchButton.waitForExists(waitingTime))
assertFalse(voiceSearchButton.waitForExists(waitingTimeShort))
}
}
// Device or AVD requires a Google Services Android OS installation
fun startVoiceSearch() {
voiceSearchButton.click()
grantSystemPermission()
// Accept runtime permission (API 30) for Google Voice
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
val allowPermission = mDevice.findObject(
UiSelector().text(
when {
Build.VERSION.SDK_INT == Build.VERSION_CODES.R -> "Allow all the time"
else -> "While using the app"
},
),
)
if (allowPermission.exists()) {
allowPermission.click()
}
if (isPackageInstalled(Constants.PackageName.GOOGLE_QUICK_SEARCH)) {
Intents.intended(IntentMatchers.hasAction(SPEECH_RECOGNITION))
}
if (isPackageInstalled(Constants.PackageName.GOOGLE_QUICK_SEARCH)) {
Intents.intended(IntentMatchers.hasAction(SPEECH_RECOGNITION))
}
}
@ -143,7 +129,7 @@ class SearchRobot {
for (searchSuggestion in searchSuggestions) {
assertFalse(
mDevice.findObject(UiSelector().textContains(searchSuggestion))
.waitForExists(waitingTime),
.waitForExists(waitingTimeShort),
)
}
}
@ -205,12 +191,11 @@ class SearchRobot {
rule.selectDefaultSearchEngine(searchEngineName)
fun clickSearchEngineShortcutButton() {
val searchEnginesShortcutButton = mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/search_engines_shortcut_button"),
)
searchEnginesShortcutButton.waitForExists(waitingTime)
searchEnginesShortcutButton.click()
itemWithResId("$packageName:id/search_engines_shortcut_button").also {
it.waitForExists(waitingTime)
it.click()
}
mDevice.waitForIdle(waitingTimeShort)
}
fun clickScanButton() =

@ -226,8 +226,11 @@ class SettingsRobot {
fun openSearchSubMenu(interact: SettingsSubMenuSearchRobot.() -> Unit):
SettingsSubMenuSearchRobot.Transition {
fun searchEngineButton() = onView(withText("Search"))
searchEngineButton().click()
itemWithText(getStringResource(R.string.preferences_search))
.also {
it.waitForExists(waitingTimeShort)
it.click()
}
SettingsSubMenuSearchRobot().interact()
return SettingsSubMenuSearchRobot.Transition()

@ -1,17 +1,23 @@
/* 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.robots
import android.view.View
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
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.rule.ActivityTestRule
import org.hamcrest.CoreMatchers.allOf
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestHelper.registerAndCleanupIdlingResources
import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource
import org.mozilla.fenix.helpers.click
/**
@ -29,9 +35,16 @@ class SettingsSubMenuAddonsManagerAddonDetailedMenuRobot {
return SettingsSubMenuAddonsManagerRobot.Transition()
}
fun removeAddon(interact: SettingsSubMenuAddonsManagerRobot.() -> Unit): SettingsSubMenuAddonsManagerRobot.Transition {
removeAddonButton().check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
removeAddonButton().click()
fun removeAddon(activityTestRule: ActivityTestRule<HomeActivity>, interact: SettingsSubMenuAddonsManagerRobot.() -> Unit): SettingsSubMenuAddonsManagerRobot.Transition {
registerAndCleanupIdlingResources(
ViewVisibilityIdlingResource(
activityTestRule.activity.findViewById(R.id.addon_container),
View.VISIBLE,
),
) {
removeAddonButton().check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
removeAddonButton().click()
}
SettingsSubMenuAddonsManagerRobot().interact()
return SettingsSubMenuAddonsManagerRobot.Transition()

@ -36,6 +36,7 @@ import org.mozilla.fenix.helpers.Constants.RETRY_COUNT
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
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.appName
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.restartApp
@ -66,7 +67,7 @@ class SettingsSubMenuAddonsManagerRobot {
try {
assertFalse(
mDevice.findObject(UiSelector().text("Failed to install $addonName"))
.waitForExists(waitingTime),
.waitForExists(waitingTimeShort),
)
assertTrue(

@ -22,6 +22,7 @@ import org.hamcrest.Matchers.contains
import org.junit.Assert.assertFalse
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click
@ -59,7 +60,7 @@ class SettingsSubMenuEnhancedTrackingProtectionExceptionsRobot {
} else {
assertFalse(
mDevice.findObject(UiSelector().textContains(siteUrl))
.waitForExists(waitingTime),
.waitForExists(waitingTimeShort),
)
}
}

@ -29,7 +29,9 @@ import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.endsWith
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
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.getStringResource
import org.mozilla.fenix.helpers.TestHelper.hasCousin
import org.mozilla.fenix.helpers.TestHelper.mDevice
@ -118,16 +120,12 @@ class SettingsSubMenuSearchRobot {
.perform(click())
}
fun toggleShowSearchShortcuts() {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText("Show search engines")),
),
)
onView(withText("Show search engines"))
.perform(click())
}
fun toggleShowSearchShortcuts() =
itemContainingText(getStringResource(R.string.preferences_show_search_engines))
.also {
it.waitForExists(waitingTimeShort)
it.click()
}
fun toggleVoiceSearch() {
onView(withId(androidx.preference.R.id.recycler_view)).perform(

@ -11,19 +11,24 @@ import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.BundleMatchers
import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withResourceName
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.Matchers.allOf
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdAndTextExists
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.MatcherHelper.itemWithResIdContainingText
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.ext.waitNotNull
class ShareOverlayRobot {
@ -46,7 +51,40 @@ class ShareOverlayRobot {
}
// This function verifies the share layout when a single tab is shared - no tab info shown
fun verifyShareTabLayout() = assertShareTabLayout()
fun verifyShareTabLayout() {
assertItemWithResIdExists(
// Share layout
itemWithResId("$packageName:id/sharingLayout"),
// Send to device section
itemWithResId("$packageName:id/devicesList"),
// Recently used section
itemWithResId("$packageName:id/recentAppsContainer"),
// All actions sections
itemWithResId("$packageName:id/appsList"),
)
assertItemWithResIdAndTextExists(
// Send to device header
itemWithResIdContainingText(
"$packageName:id/accountHeaderText",
getStringResource(R.string.share_device_subheader),
),
// Recently used header
itemWithResIdContainingText(
"$packageName:id/recent_apps_link_header",
getStringResource(R.string.share_link_recent_apps_subheader),
),
// All actions header
itemWithResIdContainingText(
"$packageName:id/apps_link_header",
getStringResource(R.string.share_link_all_apps_subheader),
),
)
assertItemContainingTextExists(
// Save as PDF button
itemContainingText(getStringResource(R.string.share_save_to_pdf)),
)
}
// this verifies the Android sharing layout - not customized for sharing tabs
fun verifyAndroidShareLayout() {
@ -61,10 +99,6 @@ class ShareOverlayRobot {
}
}
fun verifySendToDeviceTitle() = assertSendToDeviceTitle()
fun verifyShareALinkTitle() = assertShareALinkTitle()
fun verifySharedTabsIntent(text: String, subject: String) {
Intents.intended(
allOf(
@ -112,29 +146,3 @@ fun shareOverlay(interact: ShareOverlayRobot.() -> Unit): ShareOverlayRobot.Tran
ShareOverlayRobot().interact()
return ShareOverlayRobot.Transition()
}
private fun shareTabsLayout() = onView(withResourceName("shareWrapper"))
private fun assertShareTabLayout() =
shareTabsLayout().check(matches(isDisplayed()))
private fun sendToDeviceTitle() =
onView(
allOf(
withText("SEND TO DEVICE"),
withResourceName("accountHeaderText"),
),
)
private fun assertSendToDeviceTitle() = sendToDeviceTitle()
.check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun shareALinkTitle() =
onView(
allOf(
withText("ALL ACTIONS"),
withResourceName("apps_link_header"),
),
)
private fun assertShareALinkTitle() = shareALinkTitle()

@ -16,6 +16,7 @@ import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
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
@ -26,8 +27,10 @@ import org.mozilla.fenix.helpers.TestHelper.packageName
class SiteSecurityRobot {
fun verifyQuickActionSheet(url: String = "", isConnectionSecure: Boolean) = assertQuickActionSheet(url, isConnectionSecure)
fun openSecureConnectionSubMenu(isConnectionSecure: Boolean) =
quickActionSheetSecurityInfo(isConnectionSecure).clickAndWaitForNewWindow(waitingTime)
fun openSecureConnectionSubMenu(isConnectionSecure: Boolean) {
quickActionSheetSecurityInfo(isConnectionSecure).click()
mDevice.waitForWindowUpdate(packageName, waitingTimeShort)
}
fun verifySecureConnectionSubMenu(pageTitle: String = "", url: String = "", isConnectionSecure: Boolean) =
assertSecureConnectionSubMenu(pageTitle, url, isConnectionSecure)
fun clickQuickActionSheetClearSiteData() = quickActionSheetClearSiteData().click()

@ -15,10 +15,13 @@ import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.GeneralLocation
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.swipeLeft
import androidx.test.espresso.action.ViewActions.swipeRight
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.RootMatchers
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
@ -38,11 +41,13 @@ import org.hamcrest.CoreMatchers.containsString
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.assertItemWithResIdAndTextExists
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.MatcherHelper.itemWithResIdAndDescription
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
@ -125,20 +130,62 @@ class TabDrawerRobot {
}
fun swipeTabRight(title: String) {
var retries = 0 // number of retries before failing, will stop at 2
while (!tabItem(title).waitUntilGone(waitingTimeShort) && retries < 3
) {
tab(title).swipeRight(3)
retries++
for (i in 1..RETRY_COUNT) {
try {
onView(
allOf(
withId(R.id.tab_item),
hasDescendant(
allOf(
withId(R.id.mozac_browser_tabstray_title),
withText(title),
),
),
),
).perform(swipeRight())
assertTrue(
itemWithResIdContainingText(
"$packageName:id/mozac_browser_tabstray_title",
title,
).waitUntilGone(waitingTimeShort),
)
break
} catch (e: AssertionError) {
if (i == RETRY_COUNT) {
throw e
}
}
}
}
fun swipeTabLeft(title: String) {
var retries = 0 // number of retries before failing, will stop at 2
while (!tabItem(title).waitUntilGone(waitingTimeShort) && retries < 3
) {
tab(title).swipeLeft(3)
retries++
for (i in 1..RETRY_COUNT) {
try {
onView(
allOf(
withId(R.id.tab_item),
hasDescendant(
allOf(
withId(R.id.mozac_browser_tabstray_title),
withText(title),
),
),
),
).perform(swipeLeft())
assertTrue(
itemWithResIdContainingText(
"$packageName:id/mozac_browser_tabstray_title",
title,
).waitUntilGone(waitingTimeShort),
)
break
} catch (e: AssertionError) {
if (i == RETRY_COUNT) {
throw e
}
}
}
}
@ -300,15 +347,14 @@ class TabDrawerRobot {
tabPosition: Int,
interact: BrowserRobot.() -> Unit,
): BrowserRobot.Transition {
val tab = mDevice.findObject(
mDevice.findObject(
UiSelector()
.className("androidx.compose.ui.platform.ComposeView")
.resourceId("$packageName:id/tab_tray_grid_item")
.index(tabPosition),
)
UiScrollable(UiSelector().resourceId("$packageName:id/tray_list_item")).scrollIntoView(tab)
tab.waitForExists(waitingTime)
tab.click()
).also {
it.waitForExists(waitingTime)
it.click()
}
BrowserRobot().interact()
return BrowserRobot.Transition()
@ -450,7 +496,7 @@ private fun assertExistingOpenTabs(vararg tabTitles: String) {
private fun assertNoExistingOpenTabs(vararg tabTitles: String) {
for (title in tabTitles) {
assertFalse(
tabItem(title).waitForExists(waitingTimeLong),
tabItem(title).waitForExists(waitingTimeShort),
)
}
}
@ -567,10 +613,6 @@ private fun assertSyncedTabsButtonIsSelected(isSelected: Boolean) {
private val tabsList =
UiScrollable(UiSelector().className("androidx.recyclerview.widget.RecyclerView"))
// This Espresso tab selector is used for actions that UIAutomator doesn't handle very well: swipe and long-tap
private fun tab(title: String) =
mDevice.findObject(UiSelector().textContains(title))
// This tab selector is used for actions that involve waiting and asserting the existence of the view
private fun tabItem(title: String) =
mDevice.findObject(

@ -3,6 +3,17 @@
- 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/. -->
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:shortcutId="password_manager"
android:enabled="true"
android:icon="@drawable/ic_static_password_shortcut"
android:shortcutShortLabel="@string/home_screen_shortcut_open_password_screen"
android:shortcutLongLabel="@string/home_screen_shortcut_open_password_screen">
<intent
android:action="org.mozilla.fenix.OPEN_PASSWORD_MANAGER"
android:targetPackage="org.mozilla.firefox_beta"
android:targetClass="org.mozilla.fenix.IntentReceiverActivity" />
</shortcut>
<shortcut
android:shortcutId="open_new_tab"
android:enabled="true"

@ -4,6 +4,17 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:shortcutId="password_manager"
android:enabled="true"
android:icon="@drawable/ic_static_password_shortcut"
android:shortcutShortLabel="@string/home_screen_shortcut_open_password_screen"
android:shortcutLongLabel="@string/home_screen_shortcut_open_password_screen">
<intent
android:action="org.mozilla.fenix.OPEN_PASSWORD_MANAGER"
android:targetPackage="org.mozilla.fenix.debug"
android:targetClass="org.mozilla.fenix.IntentReceiverActivity" />
</shortcut>
<shortcut
android:shortcutId="open_new_tab"
android:enabled="true"

@ -34,6 +34,7 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) {
FromAddNewDeviceFragment(R.id.addNewDeviceFragment),
FromAddSearchEngineFragment(R.id.addSearchEngineFragment),
FromEditCustomSearchEngineFragment(R.id.editCustomSearchEngineFragment),
FromSaveSearchEngineFragment(R.id.saveSearchEngineFragment),
FromAddonDetailsFragment(R.id.addonDetailsFragment),
FromStudiesFragment(R.id.studiesFragment),
FromAddonPermissionsDetailsFragment(R.id.addonPermissionsDetailFragment),

@ -57,6 +57,11 @@ object FeatureFlags {
*/
const val composeTabsTray = false
/**
* Enables compose on the top sites.
*/
const val composeTopSites = false
/**
* Enables the save to PDF feature.
*/
@ -66,4 +71,10 @@ object FeatureFlags {
* Enables the notification pre permission prompt.
*/
const val notificationPrePermissionPromptEnabled = true
/**
* Enables new search settings UI with two extra fragments, for managing the default engine
* and managing search shortcuts in the quick search menu.
*/
const val unifiedSearchSettings = true
}

@ -76,8 +76,6 @@ import org.mozilla.fenix.components.Core
import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.components.metrics.MetricServiceType
import org.mozilla.fenix.components.metrics.MozillaProductDetector
import org.mozilla.fenix.components.metrics.clientdeduplication.ClientDeduplicationLifecycleObserver
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.experiments.maybeFetchExperiments
import org.mozilla.fenix.ext.areNotificationsEnabledSafe
import org.mozilla.fenix.ext.containsQueryParameters
@ -99,7 +97,6 @@ import org.mozilla.fenix.push.PushFxaIntegration
import org.mozilla.fenix.push.WebPushEngineIntegration
import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks
import org.mozilla.fenix.session.VisibilityLifecycleCallback
import org.mozilla.fenix.settings.CustomizationFragment
import org.mozilla.fenix.utils.BrowsersCache
import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.utils.Settings.Companion.TOP_SITES_PROVIDER_MAX_THRESHOLD
@ -205,12 +202,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
GlobalScope.launch(Dispatchers.IO) {
setStartupMetrics(store, settings())
}
ProcessLifecycleOwner.get().lifecycle.addObserver(
ClientDeduplicationLifecycleObserver(
this.applicationContext,
),
)
}
@VisibleForTesting
@ -784,13 +775,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
mobileBookmarksCount.add(mobileBookmarksSize)
}
toolbarPosition.set(
when (settings.toolbarPosition) {
ToolbarPosition.BOTTOM -> CustomizationFragment.Companion.Position.BOTTOM.name
ToolbarPosition.TOP -> CustomizationFragment.Companion.Position.TOP.name
},
)
tabViewSetting.set(settings.getTabViewPingString())
closeTabSetting.set(settings.getTabTimeoutPingString())

@ -6,6 +6,7 @@ package org.mozilla.fenix
import androidx.navigation.NavDirections
import mozilla.appservices.places.BookmarkRoot
import org.mozilla.fenix.components.accounts.FenixFxAEntryPoint
/**
* Used with [HomeActivity] global navigation to indicate which fragment is being opened.
@ -28,7 +29,7 @@ enum class GlobalDirections(val navDirections: NavDirections, val destinationId:
R.id.settingsFragment,
),
Sync(
NavGraphDirections.actionGlobalTurnOnSync(),
NavGraphDirections.actionGlobalTurnOnSync(entrypoint = FenixFxAEntryPoint.DeepLink),
R.id.turnOnSyncFragment,
),
SearchEngine(

@ -107,6 +107,7 @@ import org.mozilla.fenix.home.intent.AssistIntentProcessor
import org.mozilla.fenix.home.intent.CrashReporterIntentProcessor
import org.mozilla.fenix.home.intent.HomeDeepLinkIntentProcessor
import org.mozilla.fenix.home.intent.OpenBrowserIntentProcessor
import org.mozilla.fenix.home.intent.OpenPasswordManagerIntentProcessor
import org.mozilla.fenix.home.intent.OpenSpecificTabIntentProcessor
import org.mozilla.fenix.home.intent.ReEngagementIntentProcessor
import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor
@ -142,6 +143,7 @@ import org.mozilla.fenix.settings.logins.fragment.SavedLoginsAuthFragmentDirecti
import org.mozilla.fenix.settings.quicksettings.protections.cookiebanners.dialog.CookieBannerReEngagementDialogUtils
import org.mozilla.fenix.settings.search.AddSearchEngineFragmentDirections
import org.mozilla.fenix.settings.search.EditCustomSearchEngineFragmentDirections
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
@ -203,6 +205,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
StartSearchIntentProcessor(),
OpenBrowserIntentProcessor(this, ::getIntentSessionId),
OpenSpecificTabIntentProcessor(this),
OpenPasswordManagerIntentProcessor(),
ReEngagementIntentProcessor(this, settings()),
)
}
@ -279,7 +282,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
) {
// Unless activity is recreated due to config change, navigate to onboarding
if (savedInstanceState == null) {
navHost.navController.navigate(NavGraphDirections.actionGlobalHomeJunoOnboarding())
navHost.navController.navigate(NavGraphDirections.actionGlobalJunoOnboarding())
}
} else {
lifecycleScope.launch(IO) {
@ -960,6 +963,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
AddSearchEngineFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromEditCustomSearchEngineFragment ->
EditCustomSearchEngineFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromSaveSearchEngineFragment ->
SaveSearchEngineFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromAddonDetailsFragment ->
AddonDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromAddonPermissionsDetailsFragment ->
@ -1068,7 +1073,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
}
open fun navigateToHome() {
navHost.navController.navigate(NavGraphDirections.actionStartupHome())
if (components.fenixOnboarding.userHasBeenOnboarded()) {
navHost.navController.navigate(NavGraphDirections.actionStartupHome())
} else {
navHost.navController.navigate(NavGraphDirections.actionStartupOnboarding())
}
}
override fun attachBaseContext(base: Context) {
@ -1234,6 +1243,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
const val OPEN_TO_SEARCH = "open_to_search"
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"
// PWA must have been used within last 30 days to be considered "recently used" for the
// telemetry purposes.

@ -114,6 +114,7 @@ class IntentReceiverActivity : Activity() {
components.intentProcessors.fennecPageShortcutIntentProcessor +
components.intentProcessors.externalDeepLinkIntentProcessor +
components.intentProcessors.webNotificationsIntentProcessor +
components.intentProcessors.passwordManagerIntentProcessor +
modeDependentProcessors +
NewTabShortcutIntentProcessor()
}

@ -101,6 +101,7 @@ import mozilla.components.support.ktx.kotlin.getOrigin
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import mozilla.components.support.locale.ActivityContextWrapper
import mozilla.components.ui.widgets.withCenterAlignedButtons
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.GleanMetrics.MediaState
@ -621,6 +622,15 @@ abstract class BaseBrowserFragment :
launchInApp = { context.settings().shouldOpenLinksInApp(customTabSessionId != null) },
loadUrlUseCase = context.components.useCases.sessionUseCases.loadUrl,
shouldPrompt = { context.settings().shouldPromptOpenLinksInApp() },
failedToLaunchAction = { fallbackUrl ->
fallbackUrl?.let {
val appLinksUseCases = activity.components.useCases.appLinksUseCases
val getRedirect = appLinksUseCases.appLinkRedirect
val redirect = getRedirect.invoke(fallbackUrl)
redirect.appIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK
appLinksUseCases.openAppLink.invoke(redirect.appIntent)
}
},
),
owner = this,
view = view,
@ -959,7 +969,7 @@ abstract class BaseBrowserFragment :
}
create()
}.show().secure(activity)
}.show().withCenterAlignedButtons().secure(activity)
context.settings().incrementSecureWarningCount()
}

@ -13,6 +13,8 @@ import android.widget.FrameLayout
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.doOnNextLayout
import androidx.core.view.updateLayoutParams
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.thumbnails.loader.ThumbnailLoader
import mozilla.components.concept.base.images.ImageLoadRequest
import org.mozilla.fenix.R
@ -50,6 +52,13 @@ class TabPreview @JvmOverloads constructor(
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
val store = context.components.core.store
store.state.selectedTab?.let {
val count = store.state.getNormalOrPrivateTabs(it.content.private).size
binding.tabButton.setCount(count)
}
binding.previewThumbnail.translationY = if (!context.settings().shouldUseBottomToolbar) {
binding.fakeToolbar.height.toFloat()
} else {

@ -15,6 +15,7 @@ import kotlinx.coroutines.launch
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.support.ktx.android.view.showKeyboard
import mozilla.components.ui.widgets.withCenterAlignedButtons
import org.mozilla.fenix.R
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.ext.getDefaultCollectionNumber
@ -79,7 +80,7 @@ fun CollectionsDialog.show(
dialog.cancel()
}
val dialog = builder.create()
val dialog = builder.create().withCenterAlignedButtons()
val collectionNames =
arrayOf(context.getString(R.string.tab_tray_add_new_collection)) + collections
val collectionsListAdapter = CollectionsListAdapter(collectionNames) {
@ -126,7 +127,7 @@ internal fun CollectionsDialog.showAddNewDialog(
onNegativeButtonClick.invoke()
dialog.cancel()
}
.create()
.create().withCenterAlignedButtons()
.show()
collectionNameEditText.setSelection(0, collectionNameEditText.text.length)

@ -25,6 +25,7 @@ import org.mozilla.fenix.customtabs.FennecWebAppIntentProcessor
import org.mozilla.fenix.home.intent.FennecBookmarkShortcutsIntentProcessor
import org.mozilla.fenix.intent.ExternalDeepLinkIntentProcessor
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.shortcut.PasswordManagerIntentProcessor
/**
* Component group for miscellaneous components.
@ -80,4 +81,8 @@ class IntentProcessors(
val webNotificationsIntentProcessor by lazyMonitored {
WebNotificationIntentProcessor(engine)
}
val passwordManagerIntentProcessor by lazyMonitored {
PasswordManagerIntentProcessor()
}
}

@ -4,9 +4,9 @@
package org.mozilla.fenix.components
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.get
import mozilla.components.lib.state.Store
@ -20,9 +20,9 @@ class StoreProvider<T : Store<*, *>>(
) : ViewModel() {
companion object {
fun <T : Store<*, *>> get(fragment: Fragment, createStore: () -> T): T {
fun <T : Store<*, *>> get(owner: ViewModelStoreOwner, createStore: () -> T): T {
val factory = StoreProviderFactory(createStore)
val viewModel: StoreProvider<T> = ViewModelProvider(fragment, factory).get()
val viewModel: StoreProvider<T> = ViewModelProvider(owner, factory).get()
return viewModel.store
}
}

@ -0,0 +1,119 @@
/* 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.components.accounts
import android.os.Parcel
import android.os.Parcelable
import mozilla.components.concept.sync.FxAEntryPoint
/**
* Fenix implementation of [FxAEntryPoint].
*/
enum class FenixFxAEntryPoint(override val entryName: String) : FxAEntryPoint, Parcelable {
/**
* New user onboarding, the user accessed the sign in through new user onboarding
*/
NewUserOnboarding("newuser-onboarding"),
/**
* Manual sign in from the onboarding menu
*/
OnboardingManualSignIn("onboarding-manual-sign-in"),
/**
* User used a deep link to get to firefox accounts authentication
*/
DeepLink("deep-link"),
/**
* Authenticating from the browser's toolbar
*/
BrowserToolbar("browser-toolbar"),
/**
* Authenticating from the home menu (the hamburger menu)
*/
HomeMenu("home-menu"),
/**
* Authenticating in the bookmark view, when getting attempting to get synced
* bookmarks
*/
BookmarkView("bookmark-view"),
/**
* Authenticating from the homepage onboarding dialog
*/
HomeOnboardingDialog("home-onboarding-dialog"),
/**
* Authenticating from the settings menu
*/
SettingsMenu("settings-menu"),
/**
* Authenticating from the autofill settings to enable synced
* credit cards/addresses
*/
AutofillSetting("autofill-setting"),
/**
* Authenticating from the saved logins menu to enable synced
* logins
*/
SavedLogins("saved-logins"),
/**
* Authenticating from the Share menu to enable send tab
*/
ShareMenu("share-menu"),
/**
* Authenticating as a navigation interaction
*/
NavigationInteraction("navigation-interaction"),
/**
* Authenticating from the synced tabs menu to enable synced tabs
*/
SyncedTabsMenu("synced-tabs-menu"),
/**
* When serializing the value after navigating, the result is a nullable value. We have this
* "unknown" as a default value in the odd chance that we receive an [entryName] is not part of this enum.
*
* Do not use within app code.
*/
Unknown("unknown"),
;
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(entryName)
}
override fun describeContents(): Int {
return 0
}
/**
* Override implementation of the [Parcelable.Creator].
*
* Implementation notes: We need to manually create an override for [Parcelable] instead of using the annotation,
* because this is an enum implementation of the API and the auto-generated code does not know how to choose a
* particular enum value in [Parcelable.Creator.createFromParcel].
* We also introduce an [FxAEntryPoint.Unknown] value to use as a default return value in the off-chance that we
* cannot safely serialize the enum value from the navigation library; this should be a rare case, if any.
*/
companion object CREATOR : Parcelable.Creator<FenixFxAEntryPoint> {
override fun createFromParcel(parcel: Parcel): FenixFxAEntryPoint {
val parcelEntryName = parcel.readString() ?: Unknown
return FenixFxAEntryPoint.values().first { it.entryName == parcelEntryName }
}
override fun newArray(size: Int): Array<FenixFxAEntryPoint?> {
return arrayOfNulls(size)
}
}
}

@ -36,6 +36,7 @@ class AdjustMetricsService(private val application: Application) : MetricsServic
AdjustConfig.ENVIRONMENT_PRODUCTION,
true,
)
config.setPreinstallTrackingEnabled(true)
val installationPing = FirstSessionPing(application)

@ -8,6 +8,8 @@ import android.content.Context
import android.net.UrlQuerySanitizer
import android.os.RemoteException
import androidx.annotation.VisibleForTesting
import com.android.installreferrer.api.InstallReferrerClient
import com.android.installreferrer.api.InstallReferrerStateListener
import org.mozilla.fenix.GleanMetrics.PlayStoreAttribution
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.utils.Settings
@ -21,6 +23,8 @@ import java.net.URLDecoder
class InstallReferrerMetricsService(private val context: Context) : MetricsService {
override val type = MetricServiceType.Marketing
private var referrerClient: InstallReferrerClient? = null
override fun start() {
if (context.settings().utmParamsKnown) {
return
@ -28,6 +32,8 @@ class InstallReferrerMetricsService(private val context: Context) : MetricsServi
}
override fun stop() {
referrerClient?.endConnection()
referrerClient = null
}
override fun track(event: Event) = Unit

@ -65,7 +65,7 @@ internal class DefaultMetricsStorage(
/**
* Checks local state to see whether the [event] should be sent.
*/
@Suppress("ComplexMethod", "CyclomaticComplexMethod")
@Suppress("CyclomaticComplexMethod")
override suspend fun shouldTrack(event: Event): Boolean =
withContext(dispatcher) {
// The side-effect of storing days of use always needs to happen.

@ -1,43 +0,0 @@
/* 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.components.metrics.clientdeduplication
import android.content.Context
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import org.mozilla.fenix.nimbus.FxNimbus
/**
* This observer allows us to mimic roughly the same schedule as the Glean SDK baseline ping.
* https://github.com/mozilla/glean/blob/main/glean-core/android/src/main/java/mozilla/telemetry/glean/scheduler/GleanLifecycleObserver.kt
*/
class ClientDeduplicationLifecycleObserver(context: Context) : LifecycleEventObserver {
private val clientDeduplicationPing = ClientDeduplicationPing(context)
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
// The ping will only be sent whenever the Nimbus feature is enabled.
if (FxNimbus.features.clientDeduplication.value().enabled) {
when (event) {
Lifecycle.Event.ON_STOP -> {
clientDeduplicationPing.triggerPing(active = false)
}
Lifecycle.Event.ON_START -> {
// We use ON_START here because we don't want to incorrectly count metrics in
// ON_RESUME as pause/resume can happen when interacting with things like the
// navigation shade which could lead to incorrectly recording the start of a
// duration, etc.
//
// https://developer.android.com/reference/android/app/Activity.html#onStart()
clientDeduplicationPing.triggerPing(active = true)
}
else -> {
// For other lifecycle events, do nothing
}
}
}
}
}

@ -1,49 +0,0 @@
/* 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.components.metrics.clientdeduplication
import android.content.Context
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.mozilla.fenix.GleanMetrics.ClientDeduplication
import org.mozilla.fenix.GleanMetrics.Pings
import org.mozilla.fenix.components.metrics.MetricsUtils.getHashedIdentifier
/**
* Class to help construct and send the `clientDeduplication` ping.
*/
class ClientDeduplicationPing(private val context: Context) {
private val customHashingSalt = "bug-1813195-02-2023"
/**
* Fills the metrics and triggers the 'clientDeduplication' ping.
*/
internal fun triggerPing(active: Boolean) {
CoroutineScope(Dispatchers.IO).launch {
val hashedId = getHashedIdentifier(context, customHashingSalt)
// Record the metrics.
if (hashedId != null) {
// We have a valid, hashed Google Advertising ID.
ClientDeduplication.hashedGaid.set(hashedId)
ClientDeduplication.validAdvertisingId.set(true)
} else {
ClientDeduplication.validAdvertisingId.set(false)
}
ClientDeduplication.experimentTimeframe.set(customHashingSalt)
// Set the reason based on if the app is foregrounded or backgrounded.
val reason = if (active) {
Pings.clientDeduplicationReasonCodes.active
} else {
Pings.clientDeduplicationReasonCodes.inactive
}
// Submit the ping.
Pings.clientDeduplication.submit(reason)
}
}
}

@ -28,6 +28,7 @@ import mozilla.components.feature.top.sites.PinnedSiteStorage
import mozilla.components.feature.top.sites.TopSite
import mozilla.components.service.glean.private.NoExtras
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.ui.widgets.withCenterAlignedButtons
import org.mozilla.fenix.GleanMetrics.Collections
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.ReaderMode
@ -41,6 +42,7 @@ import org.mozilla.fenix.collections.SaveCollectionStep
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.accounts.AccountState
import org.mozilla.fenix.components.accounts.FenixFxAEntryPoint
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.ext.nav
@ -230,9 +232,11 @@ class DefaultBrowserToolbarMenuController(
AccountState.AUTHENTICATED ->
BrowserFragmentDirections.actionGlobalAccountSettingsFragment()
AccountState.NEEDS_REAUTHENTICATION ->
BrowserFragmentDirections.actionGlobalAccountProblemFragment()
BrowserFragmentDirections.actionGlobalAccountProblemFragment(
entrypoint = FenixFxAEntryPoint.BrowserToolbar,
)
AccountState.NO_ACCOUNT ->
BrowserFragmentDirections.actionGlobalTurnOnSync()
BrowserFragmentDirections.actionGlobalTurnOnSync(entrypoint = FenixFxAEntryPoint.BrowserToolbar)
}
browserAnimator.captureEngineViewAndDrawStatically {
navController.nav(
@ -262,7 +266,7 @@ class DefaultBrowserToolbarMenuController(
setPositiveButton(R.string.top_sites_max_limit_confirmation_button) { dialog, _ ->
dialog.dismiss()
}
create()
create().withCenterAlignedButtons()
}.show()
} else {
ioScope.launch {

@ -31,15 +31,17 @@ private class LazyListEagerFlingBehavior(
private val scope: CoroutineScope,
) : FlingBehavior {
override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
val firstItemIndex = lazyRowState.firstVisibleItemIndex
if (lazyRowState.canScrollForward) {
val firstItemIndex = lazyRowState.firstVisibleItemIndex
val itemIndexToScrollTo = when (initialVelocity <= 0) {
true -> firstItemIndex
false -> firstItemIndex + 1
}
val itemIndexToScrollTo = when (initialVelocity <= 0) {
true -> firstItemIndex
false -> firstItemIndex + 1
}
scope.launch {
lazyRowState.animateScrollToItem(itemIndexToScrollTo)
scope.launch {
lazyRowState.animateScrollToItem(itemIndexToScrollTo)
}
}
return 0f // we've consumed the entire fling

@ -5,7 +5,9 @@
package org.mozilla.fenix.compose
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
@ -13,28 +15,36 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
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.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.compose.button.PrimaryButton
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Popup action dropdown menu.
* Root popup action dropdown menu.
*
* @param menuItems List of items to be displayed in the menu.
* @param showMenu Whether or not the menu is currently displayed to the user.
* @param cornerShape Shape to apply to the corners of the dropdown.
* @param onDismissRequest Invoked when user dismisses the menu or on orientation changes.
* @param modifier Modifier to be applied to the menu.
* @param offset Offset to be added to the position of the menu.
*/
@Composable
fun DropdownMenu(
private fun Menu(
menuItems: List<MenuItem>,
showMenu: Boolean,
cornerShape: RoundedCornerShape,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
offset: DpOffset = DpOffset.Zero,
@ -43,10 +53,10 @@ fun DropdownMenu(
onDispose { onDismissRequest() }
}
MaterialTheme(shapes = MaterialTheme.shapes.copy(medium = RoundedCornerShape(2.dp))) {
MaterialTheme(shapes = MaterialTheme.shapes.copy(medium = cornerShape)) {
DropdownMenu(
expanded = showMenu && menuItems.isNotEmpty(),
onDismissRequest = { onDismissRequest() },
onDismissRequest = onDismissRequest,
offset = offset,
modifier = Modifier
.background(color = FirefoxTheme.colors.layer2)
@ -54,6 +64,7 @@ fun DropdownMenu(
) {
for (item in menuItems) {
DropdownMenuItem(
modifier = Modifier.testTag(item.testTag),
onClick = {
onDismissRequest()
item.onClick()
@ -74,31 +85,67 @@ fun DropdownMenu(
}
}
/**
* Dropdown menu for presenting context-specific actions.
*
* @param menuItems List of items to be displayed in the menu.
* @param showMenu Whether or not the menu is currently displayed to the user.
* @param onDismissRequest Invoked when user dismisses the menu or on orientation changes.
* @param modifier Modifier to be applied to the menu.
* @param offset Offset to be added to the position of the menu.
*/
@Composable
fun ContextualMenu(
menuItems: List<MenuItem>,
showMenu: Boolean,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
offset: DpOffset = DpOffset.Zero,
) {
Menu(
menuItems = menuItems,
showMenu = showMenu,
cornerShape = RoundedCornerShape(size = 5.dp),
onDismissRequest = onDismissRequest,
modifier = modifier,
offset = offset,
)
}
/**
* Represents a text item from the dropdown menu.
*
* @property title Text the item should display.
* @property color Color used to display the text.
* @property testTag Tag used to identify the item in automated tests.
* @property onClick Callback to be called when the item is clicked.
*/
data class MenuItem(
val title: String,
val color: Color? = null,
val testTag: String = "",
val onClick: () -> Unit,
)
@LightDarkPreview
@Composable
private fun DropdownMenuPreview() {
private fun ContextualMenuPreview() {
var showMenu by remember { mutableStateOf(false) }
FirefoxTheme {
DropdownMenu(
listOf(
MenuItem("Rename") {},
MenuItem("Share") {},
MenuItem("Remove", FirefoxTheme.colors.textWarning) {},
),
true,
{},
)
Box(modifier = Modifier.size(400.dp)) {
PrimaryButton(text = "Show menu") {
showMenu = true
}
ContextualMenu(
menuItems = listOf(
MenuItem("Rename") {},
MenuItem("Share") {},
MenuItem("Remove") {},
),
showMenu = showMenu,
onDismissRequest = { showMenu = false },
)
}
}
}

@ -2,8 +2,11 @@
* 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:OptIn(ExperimentalFoundationApi::class)
package org.mozilla.fenix.compose
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -13,6 +16,9 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@ -20,9 +26,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.PagerState
import com.google.accompanist.pager.rememberPagerState
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.theme.FirefoxTheme
@ -31,10 +34,10 @@ import org.mozilla.fenix.theme.FirefoxTheme
* indicators to show progress, instead of just showing the current one as active.
*
* @param pagerState The state object of your [HorizontalPager] to be used to observe the list's state.
* @param modifier The modifier to apply to this layout.
* @param pageCount The size of indicators should be displayed, defaults to [PagerState.pageCount].
* If you are implementing a looping pager with a much larger [PagerState.pageCount]
* than indicators should displayed, e.g. [Int.MAX_VALUE], specify you real size in this param.
* @param modifier The modifier to apply to this layout.
* @param activeColor The color of the active page indicator, and the color of previous page
* indicators in case [leaveTrail] is set to true.
* @param inactiveColor The color of page indicators that are inactive.
@ -44,10 +47,10 @@ import org.mozilla.fenix.theme.FirefoxTheme
@Composable
fun PagerIndicator(
pagerState: PagerState,
pageCount: Int,
modifier: Modifier = Modifier,
pageCount: Int = pagerState.pageCount,
activeColor: Color,
inactiveColor: Color,
activeColor: Color = FirefoxTheme.colors.indicatorActive,
inactiveColor: Color = FirefoxTheme.colors.indicatorInactive,
leaveTrail: Boolean = false,
) {
Row(
@ -123,6 +126,22 @@ private fun PagerIndicatorPreview() {
leaveTrail = true,
modifier = Modifier.align(Alignment.CenterHorizontally),
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Default colors",
style = FirefoxTheme.typography.caption,
color = FirefoxTheme.colors.textPrimary,
)
Spacer(modifier = Modifier.height(8.dp))
PagerIndicator(
pagerState = rememberPagerState(1),
pageCount = 3,
modifier = Modifier.align(Alignment.CenterHorizontally),
)
}
}
}

@ -22,7 +22,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import org.mozilla.fenix.R
@ -47,7 +46,7 @@ fun FloatingActionButton(
) {
FloatingActionButton(
onClick = onClick,
modifier = Modifier.testTag("button.fab").then(modifier),
modifier = modifier,
backgroundColor = FirefoxTheme.colors.actionPrimary,
contentColor = FirefoxTheme.colors.textActionPrimary,
) {

@ -31,10 +31,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.dp
@ -49,6 +52,7 @@ import org.mozilla.fenix.compose.Favicon
import org.mozilla.fenix.compose.HorizontalFadingEdgeBox
import org.mozilla.fenix.compose.ThumbnailCard
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.tabstray.TabsTrayTestTag
import org.mozilla.fenix.tabstray.ext.toDisplayTitle
import org.mozilla.fenix.theme.FirefoxTheme
@ -148,16 +152,18 @@ fun TabGridItem(
)
}
Icon(
painter = painterResource(id = R.drawable.mozac_ic_close),
contentDescription = stringResource(id = R.string.close_tab),
tint = FirefoxTheme.colors.iconPrimary,
modifier = Modifier
.clickable { onCloseClick(tab) }
.size(24.dp)
.align(Alignment.CenterVertically),
)
if (!multiSelectionEnabled) {
Icon(
painter = painterResource(id = R.drawable.mozac_ic_close),
contentDescription = stringResource(id = R.string.close_tab),
tint = FirefoxTheme.colors.iconPrimary,
modifier = Modifier
.clickable { onCloseClick(tab) }
.size(24.dp)
.align(Alignment.CenterVertically)
.testTag(TabsTrayTestTag.tabItemClose),
)
}
}
Divider()
@ -194,7 +200,10 @@ private fun Thumbnail(
Box(
modifier = Modifier
.fillMaxSize()
.background(FirefoxTheme.colors.layer2),
.background(FirefoxTheme.colors.layer2)
.semantics(mergeDescendants = true) {
testTag = TabsTrayTestTag.tabItemThumbnail
},
) {
ThumbnailCard(
url = tab.content.url,

@ -10,6 +10,7 @@ import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -21,9 +22,13 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import mozilla.components.browser.state.state.TabSessionState
@ -33,6 +38,7 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.compose.ThumbnailCard
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.tabstray.TabsTrayTestTag
import org.mozilla.fenix.tabstray.ext.toDisplayTitle
import org.mozilla.fenix.theme.FirefoxTheme
@ -77,7 +83,7 @@ fun TabListItem(
onLongClick = { onLongClick(tab) },
onClick = { onClick(tab) },
)
.padding(horizontal = 16.dp, vertical = 8.dp),
.padding(start = 16.dp, top = 8.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Thumbnail(
@ -89,27 +95,34 @@ fun TabListItem(
Column(
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(start = 12.dp)
.weight(weight = 1f),
) {
Text(
text = tab.toDisplayTitle().take(MAX_URI_LENGTH),
color = FirefoxTheme.colors.textPrimary,
fontSize = 16.sp,
letterSpacing = 0.0.sp,
overflow = TextOverflow.Ellipsis,
maxLines = 2,
color = FirefoxTheme.colors.textPrimary,
)
Text(
text = tab.content.url.toShortUrl(),
fontSize = 12.sp,
color = FirefoxTheme.colors.textSecondary,
fontSize = 14.sp,
letterSpacing = 0.0.sp,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
if (!multiSelectionEnabled) {
IconButton(
onClick = { onCloseClick(tab) },
modifier = Modifier.size(size = 24.dp),
modifier = Modifier
.size(size = 48.dp)
.testTag(TabsTrayTestTag.tabItemClose),
) {
Icon(
painter = painterResource(id = R.drawable.mozac_ic_close),
@ -120,6 +133,8 @@ fun TabListItem(
tint = FirefoxTheme.colors.iconPrimary,
)
}
} else {
Spacer(modifier = Modifier.size(48.dp))
}
}
}
@ -135,7 +150,11 @@ private fun Thumbnail(
ThumbnailCard(
url = tab.content.url,
key = tab.id,
modifier = Modifier.size(width = 92.dp, height = 72.dp),
modifier = Modifier
.size(width = 92.dp, height = 72.dp)
.semantics(mergeDescendants = true) {
testTag = TabsTrayTestTag.tabItemThumbnail
},
contentDescription = stringResource(id = R.string.mozac_browser_tabstray_open_tab),
)

@ -11,7 +11,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.DialogFragment
import androidx.navigation.fragment.navArgs
import org.mozilla.fenix.R
@ -54,8 +53,6 @@ class ResearchSurfaceDialogFragment : DialogFragment() {
container: ViewGroup?,
savedInstanceState: Bundle?,
): View = ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
val messageText = bundleArgs.getString(KEY_MESSAGE_TEXT)
?: getString(R.string.nimbus_survey_message_text)
val acceptButtonText = bundleArgs.getString(KEY_ACCEPT_BUTTON_TEXT)

@ -156,23 +156,6 @@ private fun Activity.openDefaultBrowserSumoPage(
}
}
/**
* Checks for the presence of an activity before starting it. In case it's not present,
* [onActivityNotPresent] is invoked, preventing ActivityNotFoundException from being thrown.
* This is useful when navigating to external activities like device permission settings,
* notification settings, default app settings, etc.
*
* @param intent The Intent of the activity to resolve and start.
* @param onActivityNotPresent Invoked when the activity to handle the intent is not present.
*/
inline fun Activity.startExternalActivitySafe(intent: Intent, onActivityNotPresent: () -> Unit) {
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
} else {
onActivityNotPresent()
}
}
/**
* Sets the icon for the back (up) navigation button.
* @param icon The resource id of the icon.

@ -88,9 +88,13 @@ val Context.accessibilityManager: AccessibilityManager get() =
getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
/**
* Used to navigate to system notifications settings for app
* Used to navigate to system notifications settings for app.
*
* @param onError Invoked when the activity described by the intent is not present on the device.
*/
fun Context.navigateToNotificationsSettings() {
fun Context.navigateToNotificationsSettings(
onError: () -> Unit,
) {
val intent = Intent()
intent.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -102,7 +106,24 @@ fun Context.navigateToNotificationsSettings() {
it.putExtra("app_uid", this.applicationInfo.uid)
}
}
startActivity(intent)
startExternalActivitySafe(intent, onError)
}
/**
* Checks for the presence of an activity before starting it. In case it's not present,
* [onActivityNotPresent] is invoked, preventing ActivityNotFoundException from being thrown.
* This is useful when navigating to external activities like device permission settings,
* notification settings, default app settings, etc.
*
* @param intent The Intent of the activity to resolve and start.
* @param onActivityNotPresent Invoked when the activity to handle the intent is not present.
*/
inline fun Context.startExternalActivitySafe(intent: Intent, onActivityNotPresent: () -> Unit) {
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
} else {
onActivityNotPresent()
}
}
/**

@ -7,16 +7,6 @@ package org.mozilla.fenix.ext
import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.settings.SupportUtils
/**
* Returns the type name of the [TopSite].
*/
fun TopSite.name(): String = when (this) {
is TopSite.Default -> "DEFAULT"
is TopSite.Frecent -> "FRECENT"
is TopSite.Pinned -> "PINNED"
is TopSite.Provided -> "PROVIDED"
}
/**
* Returns a sorted list of [TopSite] with the default Google top site always appearing
* as the first item.

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

@ -104,7 +104,6 @@ import org.mozilla.fenix.messaging.DefaultMessageController
import org.mozilla.fenix.messaging.FenixNimbusMessagingController
import org.mozilla.fenix.messaging.MessagingFeature
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.onboarding.controller.DefaultOnboardingController
import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks
import org.mozilla.fenix.perf.runBlockingIncrement
import org.mozilla.fenix.search.toolbar.DefaultSearchSelectorController
@ -183,13 +182,10 @@ class HomeFragment : Fragment() {
private var sessionControlView: SessionControlView? = null
private var tabCounterView: TabCounterView? = null
private var toolbarView: ToolbarView? = null
private var appBarLayout: AppBarLayout? = null
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal var homeMenuView: HomeMenuView? = null
private lateinit var currentMode: CurrentMode
private var lastAppliedWallpaperName: String = Wallpaper.defaultName
private val topSitesFeature = ViewBoundFeatureWrapper<TopSitesFeature>()
@ -233,14 +229,7 @@ class HomeFragment : Fragment() {
val currentWallpaperName = requireContext().settings().currentWallpaperName
applyWallpaper(wallpaperName = currentWallpaperName, orientationChange = false)
currentMode = CurrentMode(
requireContext(),
requireComponents.fenixOnboarding,
browsingModeManager,
::dispatchModeChanges,
)
components.appStore.dispatch(AppAction.ModeChange(currentMode.getCurrentMode()))
components.appStore.dispatch(AppAction.ModeChange(Mode.fromBrowsingMode(browsingModeManager.mode)))
lifecycleScope.launch(IO) {
if (requireContext().settings().showPocketRecommendationsFeature) {
@ -397,10 +386,6 @@ class HomeFragment : Fragment() {
appStore = components.appStore,
navController = findNavController(),
),
onboardingController = DefaultOnboardingController(
activity = activity,
hideOnboarding = ::hideOnboardingAndOpenSearch,
),
searchSelectorController = DefaultSearchSelectorController(
activity = activity,
navController = findNavController(),
@ -426,8 +411,6 @@ class HomeFragment : Fragment() {
updateSessionControlView()
appBarLayout = binding.homeAppBar
disableAppBarDragging()
activity.themeManager.applyStatusBarTheme(activity)
@ -533,7 +516,6 @@ class HomeFragment : Fragment() {
homeActivity = activity as HomeActivity,
navController = findNavController(),
menuButton = WeakReference(binding.menuButton),
hideOnboardingIfNeeded = ::hideOnboardingIfNeeded,
).also { it.build() }
tabCounterView = TabCounterView(
@ -548,7 +530,7 @@ class HomeFragment : Fragment() {
PrivateBrowsingButtonView(binding.privateBrowsingButton, browsingModeManager) { newMode ->
sessionControlInteractor.onPrivateModeButtonClicked(
newMode,
requireComponents.fenixOnboarding.userHasBeenOnboarded(),
userHasBeenOnboarded = true,
)
}
@ -703,7 +685,6 @@ class HomeFragment : Fragment() {
sessionControlView = null
tabCounterView = null
toolbarView = null
appBarLayout = null
_binding = null
bundleArgs.clear()
@ -723,10 +704,6 @@ class HomeFragment : Fragment() {
return@runIfReadyOrQueue
}
requireComponents.backgroundServices.accountManager.register(
currentMode,
owner = this@HomeFragment.viewLifecycleOwner,
)
requireComponents.backgroundServices.accountManager.register(
object : AccountObserver {
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
@ -764,12 +741,6 @@ class HomeFragment : Fragment() {
}
}
private fun dispatchModeChanges(mode: Mode) {
if (mode != Mode.fromBrowsingMode(browsingModeManager.mode)) {
requireContext().components.appStore.dispatch(AppAction.ModeChange(mode))
}
}
@VisibleForTesting
internal fun removeCollectionWithUndo(tabCollection: TabCollection) {
val snackbarMessage = getString(R.string.snackbar_collection_deleted)
@ -872,23 +843,6 @@ class HomeFragment : Fragment() {
}
}
private fun hideOnboardingIfNeeded() {
if (!requireComponents.fenixOnboarding.userHasBeenOnboarded()) {
requireComponents.fenixOnboarding.finish()
requireContext().components.appStore.dispatch(
AppAction.ModeChange(
mode = currentMode.getCurrentMode(),
),
)
}
}
private fun hideOnboardingAndOpenSearch() {
hideOnboardingIfNeeded()
appBarLayout?.setExpanded(true, true)
sessionControlInteractor.onNavigateSearch()
}
private fun subscribeToTabCollections(): Observer<List<TabCollection>> {
return Observer<List<TabCollection>> {
requireComponents.core.tabCollectionStorage.cachedTabCollections = it

@ -14,6 +14,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.menu.view.MenuButton
import mozilla.components.concept.sync.FxAEntryPoint
import mozilla.components.service.glean.private.NoExtras
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.Events
@ -22,6 +23,7 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.accounts.AccountState
import org.mozilla.fenix.components.accounts.FenixFxAEntryPoint
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.settings.SupportUtils
@ -51,7 +53,8 @@ class HomeMenuView(
private val homeActivity: HomeActivity,
private val navController: NavController,
private val menuButton: WeakReference<MenuButton>,
private val hideOnboardingIfNeeded: () -> Unit,
private val hideOnboardingIfNeeded: () -> Unit = {},
private val fxaEntrypoint: FxAEntryPoint = FenixFxAEntryPoint.HomeMenu,
) {
/**
@ -115,9 +118,13 @@ class HomeMenuView(
AccountState.AUTHENTICATED ->
HomeFragmentDirections.actionGlobalAccountSettingsFragment()
AccountState.NEEDS_REAUTHENTICATION ->
HomeFragmentDirections.actionGlobalAccountProblemFragment()
HomeFragmentDirections.actionGlobalAccountProblemFragment(
entrypoint = fxaEntrypoint as FenixFxAEntryPoint,
)
AccountState.NO_ACCOUNT ->
HomeFragmentDirections.actionGlobalTurnOnSync()
HomeFragmentDirections.actionGlobalTurnOnSync(
entrypoint = fxaEntrypoint as FenixFxAEntryPoint,
)
},
)
}
@ -187,7 +194,9 @@ class HomeMenuView(
HomeMenu.Item.ReconnectSync -> {
navController.nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalAccountProblemFragment(),
HomeFragmentDirections.actionGlobalAccountProblemFragment(
entrypoint = fxaEntrypoint as FenixFxAEntryPoint,
),
)
}
HomeMenu.Item.Extensions -> {

@ -4,17 +4,7 @@
package org.mozilla.fenix.home
import android.content.Context
import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.Profile
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.onboarding.FenixOnboarding
import org.mozilla.fenix.onboarding.OnboardingState
import org.mozilla.fenix.nimbus.Onboarding as OnboardingConfig
/**
* Describes various states of the home fragment UI.
@ -22,7 +12,6 @@ import org.mozilla.fenix.nimbus.Onboarding as OnboardingConfig
sealed class Mode {
object Normal : Mode()
object Private : Mode()
data class Onboarding(val state: OnboardingState, val config: OnboardingConfig) : Mode()
companion object {
fun fromBrowsingMode(browsingMode: BrowsingMode) = when (browsingMode) {
@ -31,34 +20,3 @@ sealed class Mode {
}
}
}
class CurrentMode(
private val context: Context,
private val onboarding: FenixOnboarding,
private val browsingModeManager: BrowsingModeManager,
private val dispatchModeChanges: (mode: Mode) -> Unit,
) : AccountObserver {
private val accountManager by lazy { context.components.backgroundServices.accountManager }
private val settings by lazy { context.components.settings }
fun getCurrentMode() = if (onboarding.userHasBeenOnboarded() || settings.junoOnboardingEnabled) {
Mode.fromBrowsingMode(browsingModeManager.mode)
} else {
val account = accountManager.authenticatedAccount()
if (account != null) {
Mode.Onboarding(OnboardingState.SignedIn, onboarding.config)
} else {
Mode.Onboarding(OnboardingState.SignedOutNoAutoSignIn, onboarding.config)
}
}
fun emitModeChanges() {
dispatchModeChanges(getCurrentMode())
}
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) = emitModeChanges()
override fun onAuthenticationProblems() = emitModeChanges()
override fun onLoggedOut() = emitModeChanges()
override fun onProfileUpdated(profile: Profile) = emitModeChanges()
}

@ -14,6 +14,7 @@ import mozilla.components.ui.tabcounter.TabCounter
import mozilla.components.ui.tabcounter.TabCounterMenu
import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.StartOnHome
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
@ -63,8 +64,8 @@ class TabCounterView(
StartOnHome.openTabsTray.record(NoExtras())
navController.nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalTabsTrayFragment(
navController.currentDestination?.id,
NavGraphDirections.actionGlobalTabsTrayFragment(
page = when (browsingModeManager.mode) {
BrowsingMode.Normal -> Page.NormalTabs
BrowsingMode.Private -> Page.PrivateTabs

@ -38,7 +38,7 @@ import mozilla.components.feature.tab.collections.TabCollection
import org.mozilla.fenix.R
import org.mozilla.fenix.R.drawable
import org.mozilla.fenix.R.string
import org.mozilla.fenix.compose.DropdownMenu
import org.mozilla.fenix.compose.ContextualMenu
import org.mozilla.fenix.compose.MenuItem
import org.mozilla.fenix.compose.list.ExpandableListHeader
import org.mozilla.fenix.ext.getIconColor
@ -141,7 +141,7 @@ fun Collection(
tint = FirefoxTheme.colors.iconPrimary,
)
DropdownMenu(
ContextualMenu(
showMenu = isMenuExpanded,
menuItems = menuItems,
onDismissRequest = { isMenuExpanded = false },

@ -0,0 +1,29 @@
/* 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.home.intent
import android.content.Intent
import androidx.navigation.NavController
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.ext.nav
/**
* When the open password manager shortcut is tapped, Fenix should open to the password and login fragment.
*/
class OpenPasswordManagerIntentProcessor : HomeIntentProcessor {
override fun process(intent: Intent, navController: NavController, out: Intent): Boolean {
return if (intent.extras?.getBoolean(HomeActivity.OPEN_PASSWORD_MANAGER) == true) {
out.removeExtra(HomeActivity.OPEN_PASSWORD_MANAGER)
val directions = NavGraphDirections.actionGlobalSavedLoginsAuthFragment()
navController.nav(null, directions)
true
} else {
false
}
}
}

@ -12,13 +12,13 @@ import android.text.Spanned
import android.text.method.LinkMovementMethod
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import mozilla.components.ui.widgets.withCenterAlignedButtons
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.MetricServiceType
import org.mozilla.fenix.ext.application
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import kotlin.system.exitProcess
fun showPrivacyPopWindow(context: Context, activity: Activity) {
val content = context.getString(R.string.privacy_notice_content)
@ -67,12 +67,20 @@ fun showPrivacyPopWindow(context: Context, activity: Activity) {
}
.setNeutralButton(
context.getString(R.string.privacy_notice_neutral_button_2),
{ _, _ -> exitProcess(0) },
)
) { _, _ ->
context.settings().shouldShowPrivacyPopWindow = false
context.settings().isMarketingTelemetryEnabled = false
context.settings().isTelemetryEnabled = false
context.components.analytics.metrics.start(MetricServiceType.Marketing)
// Now that the privacy notice is accepted, application initialization can continue.
context.application.initialize()
activity.startActivity(Intent(activity, HomeActivity::class.java))
activity.finish()
}
.setTitle(context.getString(R.string.privacy_notice_title))
.setMessage(messageSpannable)
.setCancelable(false)
val alertDialog: AlertDialog = builder.create()
val alertDialog: AlertDialog = builder.create().withCenterAlignedButtons()
alertDialog.show()
alertDialog.findViewById<TextView>(android.R.id.message)?.movementMethod = LinkMovementMethod.getInstance()
}

@ -45,7 +45,7 @@ import mozilla.components.browser.icons.compose.Placeholder
import mozilla.components.browser.icons.compose.WithIcon
import mozilla.components.ui.colors.PhotonColors
import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.DropdownMenu
import org.mozilla.fenix.compose.ContextualMenu
import org.mozilla.fenix.compose.Image
import org.mozilla.fenix.compose.MenuItem
import org.mozilla.fenix.compose.annotation.LightDarkPreview
@ -150,7 +150,7 @@ private fun RecentBookmarkItem(
style = FirefoxTheme.typography.caption,
)
DropdownMenu(
ContextualMenu(
showMenu = isMenuExpanded,
onDismissRequest = { isMenuExpanded = false },
menuItems = menuItems.map { item -> MenuItem(item.title) { item.onClick(bookmark) } },

@ -42,7 +42,7 @@ import androidx.compose.ui.unit.sp
import mozilla.components.concept.sync.DeviceType
import mozilla.components.support.ktx.kotlin.trimmed
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.DropdownMenu
import org.mozilla.fenix.compose.ContextualMenu
import org.mozilla.fenix.compose.Image
import org.mozilla.fenix.compose.MenuItem
import org.mozilla.fenix.compose.ThumbnailCard
@ -182,7 +182,7 @@ fun RecentSyncedTab(
}
}
DropdownMenu(
ContextualMenu(
showMenu = isDropdownExpanded && tab != null,
onDismissRequest = { isDropdownExpanded = false },
menuItems = listOf(

@ -53,7 +53,7 @@ import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.support.ktx.kotlin.trimmed
import mozilla.components.ui.colors.PhotonColors
import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.DropdownMenu
import org.mozilla.fenix.compose.ContextualMenu
import org.mozilla.fenix.compose.Image
import org.mozilla.fenix.compose.MenuItem
import org.mozilla.fenix.compose.ThumbnailCard
@ -191,7 +191,7 @@ private fun RecentTabItem(
}
}
DropdownMenu(
ContextualMenu(
showMenu = isMenuExpanded,
onDismissRequest = { isMenuExpanded = false },
menuItems = menuItems.map { item -> MenuItem(item.title) { item.onClick(tab) } },

@ -46,8 +46,8 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import mozilla.components.support.ktx.kotlin.trimmed
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.ContextualMenu
import org.mozilla.fenix.compose.Divider
import org.mozilla.fenix.compose.DropdownMenu
import org.mozilla.fenix.compose.EagerFlingBehavior
import org.mozilla.fenix.compose.Favicon
import org.mozilla.fenix.compose.MenuItem
@ -202,7 +202,7 @@ private fun RecentlyVisitedHistoryGroup(
}
}
DropdownMenu(
ContextualMenu(
showMenu = isMenuExpanded,
onDismissRequest = { isMenuExpanded = false },
menuItems = menuItems.map { MenuItem(it.title) { it.onClick(recentVisit) } },
@ -271,7 +271,7 @@ private fun RecentlyVisitedHistoryHighlight(
}
}
DropdownMenu(
ContextualMenu(
showMenu = isMenuExpanded,
onDismissRequest = { isMenuExpanded = false },
menuItems = menuItems.map { item -> MenuItem(item.title) { item.onClick(recentVisit) } },

@ -4,7 +4,6 @@
package org.mozilla.fenix.home.sessioncontrol
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.LayoutRes
@ -36,14 +35,6 @@ import org.mozilla.fenix.home.sessioncontrol.viewholders.CustomizeHomeButtonView
import org.mozilla.fenix.home.sessioncontrol.viewholders.NoCollectionsMessageViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.PrivateBrowsingDescriptionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.MessageCardViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingFinishViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingHeaderViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingManualSignInViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingPrivacyNoticeViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingSectionHeaderViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingThemePickerViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingToolbarPositionPickerViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingTrackingProtectionViewHolder
import org.mozilla.fenix.home.topsites.TopSitePagerViewHolder
import mozilla.components.feature.tab.collections.Tab as ComponentTab
@ -138,16 +129,6 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
}
}
object OnboardingHeader : AdapterItem(OnboardingHeaderViewHolder.LAYOUT_ID)
data class OnboardingSectionHeader(
val labelBuilder: (Context) -> String,
) : AdapterItem(OnboardingSectionHeaderViewHolder.LAYOUT_ID) {
override fun sameAs(other: AdapterItem) =
other is OnboardingSectionHeader && labelBuilder == other.labelBuilder
}
object OnboardingManualSignIn : AdapterItem(OnboardingManualSignInViewHolder.LAYOUT_ID)
data class NimbusMessageCard(
val message: Message,
) : AdapterItem(MessageCardViewHolder.LAYOUT_ID) {
@ -155,15 +136,6 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
other is NimbusMessageCard && message.id == other.message.id
}
object OnboardingThemePicker : AdapterItem(OnboardingThemePickerViewHolder.LAYOUT_ID)
object OnboardingTrackingProtection :
AdapterItem(OnboardingTrackingProtectionViewHolder.LAYOUT_ID)
object OnboardingPrivacyNotice : AdapterItem(OnboardingPrivacyNoticeViewHolder.LAYOUT_ID)
object OnboardingFinish : AdapterItem(OnboardingFinishViewHolder.LAYOUT_ID)
object OnboardingToolbarPositionPicker :
AdapterItem(OnboardingToolbarPositionPickerViewHolder.LAYOUT_ID)
object CustomizeHomeButton : AdapterItem(CustomizeHomeButtonViewHolder.LAYOUT_ID)
object RecentTabsHeader : AdapterItem(RecentTabsHeaderViewHolder.LAYOUT_ID)
@ -321,21 +293,6 @@ class SessionControlAdapter(
components.appStore,
interactor,
)
OnboardingHeaderViewHolder.LAYOUT_ID -> OnboardingHeaderViewHolder(view)
OnboardingSectionHeaderViewHolder.LAYOUT_ID -> OnboardingSectionHeaderViewHolder(view)
OnboardingManualSignInViewHolder.LAYOUT_ID -> OnboardingManualSignInViewHolder(view)
OnboardingThemePickerViewHolder.LAYOUT_ID -> OnboardingThemePickerViewHolder(view)
OnboardingTrackingProtectionViewHolder.LAYOUT_ID -> OnboardingTrackingProtectionViewHolder(
view,
)
OnboardingPrivacyNoticeViewHolder.LAYOUT_ID -> OnboardingPrivacyNoticeViewHolder(
view,
interactor,
)
OnboardingFinishViewHolder.LAYOUT_ID -> OnboardingFinishViewHolder(view, interactor)
OnboardingToolbarPositionPickerViewHolder.LAYOUT_ID -> OnboardingToolbarPositionPickerViewHolder(
view,
)
BottomSpacerViewHolder.LAYOUT_ID -> BottomSpacerViewHolder(view)
else -> throw IllegalStateException()
}
@ -424,10 +381,6 @@ class SessionControlAdapter(
val (collection, tab, isLastTab) = item as AdapterItem.TabInCollectionItem
holder.bindSession(collection, tab, isLastTab)
}
is OnboardingSectionHeaderViewHolder -> holder.bind(
(item as AdapterItem.OnboardingSectionHeader).labelBuilder,
)
is OnboardingManualSignInViewHolder,
is RecentlyVisitedViewHolder,
is RecentBookmarksViewHolder,
is RecentTabViewHolder,

@ -28,6 +28,7 @@ import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.feature.top.sites.TopSite
import mozilla.components.service.nimbus.messaging.Message
import mozilla.components.support.ktx.android.view.showKeyboard
import mozilla.components.ui.widgets.withCenterAlignedButtons
import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.Collections
@ -129,6 +130,11 @@ interface SessionControlController {
*/
fun handleSponsorPrivacyClicked()
/**
* @see [TopSiteInteractor.onTopSiteLongClicked]
*/
fun handleTopSiteLongClicked(topSite: TopSite)
/**
* @see [CollectionInteractor.onToggleCollectionExpanded]
*/
@ -306,7 +312,7 @@ class DefaultSessionControlController(
setNegativeButton(R.string.top_sites_rename_dialog_cancel) { dialog, _ ->
dialog.cancel()
}
}.show().also {
}.show().withCenterAlignedButtons().also {
topSiteLabelEditText.setSelection(0, topSiteLabelEditText.text.length)
topSiteLabelEditText.showKeyboard()
}
@ -411,6 +417,10 @@ class DefaultSessionControlController(
)
}
override fun handleTopSiteLongClicked(topSite: TopSite) {
TopSites.longPress.record(TopSites.LongPressExtra(topSite.type))
}
@VisibleForTesting
internal fun getAvailableSearchEngines() =
activity.components.core.store.state.search.searchEngines +

@ -31,8 +31,6 @@ import org.mozilla.fenix.home.recentvisits.controller.RecentVisitsController
import org.mozilla.fenix.home.recentvisits.interactor.RecentVisitsInteractor
import org.mozilla.fenix.home.toolbar.ToolbarController
import org.mozilla.fenix.home.toolbar.ToolbarInteractor
import org.mozilla.fenix.onboarding.controller.OnboardingController
import org.mozilla.fenix.onboarding.interactor.OnboardingInteractor
import org.mozilla.fenix.search.toolbar.SearchSelectorController
import org.mozilla.fenix.search.toolbar.SearchSelectorInteractor
import org.mozilla.fenix.search.toolbar.SearchSelectorMenu
@ -183,6 +181,14 @@ interface TopSiteInteractor {
* "Our sponsors & your privacy" top site menu item.
*/
fun onSponsorPrivacyClicked()
/**
* Handles long click event for the given top site. Called when an user long clicks on a top
* site.
*
* @param topSite The top site that was long clicked.
*/
fun onTopSiteLongClicked(topSite: TopSite)
}
interface MessageCardInteractor {
@ -226,11 +232,9 @@ class SessionControlInteractor(
private val recentVisitsController: RecentVisitsController,
private val pocketStoriesController: PocketStoriesController,
private val privateBrowsingController: PrivateBrowsingController,
private val onboardingController: OnboardingController,
private val searchSelectorController: SearchSelectorController,
private val toolbarController: ToolbarController,
) : CollectionInteractor,
OnboardingInteractor,
TopSiteInteractor,
TabSessionInteractor,
ToolbarInteractor,
@ -297,12 +301,8 @@ class SessionControlInteractor(
controller.handleSponsorPrivacyClicked()
}
override fun onStartBrowsingClicked() {
onboardingController.handleStartBrowsingClicked()
}
override fun onReadPrivacyNoticeClicked() {
onboardingController.handleReadPrivacyNoticeClicked()
override fun onTopSiteLongClicked(topSite: TopSite) {
controller.handleTopSiteLongClicked(topSite)
}
override fun showWallpapersOnboardingDialog(state: WallpaperState): Boolean {

@ -23,11 +23,8 @@ import org.mozilla.fenix.home.Mode
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
import org.mozilla.fenix.messaging.FenixMessageSurfaceId
import org.mozilla.fenix.nimbus.OnboardingPanel
import org.mozilla.fenix.onboarding.HomeCFRPresenter
import org.mozilla.fenix.onboarding.OnboardingState
import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.nimbus.Onboarding as OnboardingConfig
// This method got a little complex with the addition of the tab tray feature flag
// When we remove the tabs from the home screen this will get much simpler again.
@ -129,34 +126,6 @@ private fun showCollections(
private fun privateModeAdapterItems() = listOf(AdapterItem.PrivateBrowsingDescription)
private fun onboardingAdapterItems(
onboardingState: OnboardingState,
onboardingConfig: OnboardingConfig,
): List<AdapterItem> {
val items: MutableList<AdapterItem> = mutableListOf(AdapterItem.OnboardingHeader)
onboardingConfig.order.forEach {
when (it) {
OnboardingPanel.THEMES -> items.add(AdapterItem.OnboardingThemePicker)
OnboardingPanel.TOOLBAR_PLACEMENT -> items.add(AdapterItem.OnboardingToolbarPositionPicker)
// Customize FxA items based on where we are with the account state:
OnboardingPanel.SYNC -> if (onboardingState == OnboardingState.SignedOutNoAutoSignIn) {
items.add(AdapterItem.OnboardingManualSignIn)
}
OnboardingPanel.TCP -> items.add(AdapterItem.OnboardingTrackingProtection)
OnboardingPanel.PRIVACY_NOTICE -> items.add(AdapterItem.OnboardingPrivacyNotice)
}
}
items.addAll(
listOf(
AdapterItem.OnboardingFinish,
AdapterItem.BottomSpacer,
),
)
return items
}
private fun AppState.toAdapterList(settings: Settings): List<AdapterItem> = when (mode) {
is Mode.Normal -> normalModeAdapterItems(
settings,
@ -173,7 +142,6 @@ private fun AppState.toAdapterList(settings: Settings): List<AdapterItem> = when
firstFrameDrawn,
)
is Mode.Private -> privateModeAdapterItems()
is Mode.Onboarding -> onboardingAdapterItems(mode.state, mode.config)
}
private fun collectionTabItems(collection: TabCollection) =

@ -20,7 +20,7 @@ class OnboardingFinishViewHolder(
init {
val binding = OnboardingFinishBinding.bind(view)
binding.finishButton.setOnClickListener {
interactor.onStartBrowsingClicked()
interactor.onFinishOnboarding(focusOnAddressBar = true)
Onboarding.finish.record(NoExtras())
}
}

@ -10,6 +10,7 @@ import androidx.recyclerview.widget.RecyclerView
import mozilla.components.service.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.Onboarding
import org.mozilla.fenix.R
import org.mozilla.fenix.components.accounts.FenixFxAEntryPoint
import org.mozilla.fenix.databinding.OnboardingManualSigninBinding
import org.mozilla.fenix.home.HomeFragmentDirections
@ -20,7 +21,9 @@ class OnboardingManualSignInViewHolder(view: View) : RecyclerView.ViewHolder(vie
binding.fxaSignInButton.setOnClickListener {
Onboarding.fxaManualSignin.record(NoExtras())
val directions = HomeFragmentDirections.actionGlobalTurnOnSync()
val directions = HomeFragmentDirections.actionGlobalTurnOnSync(
entrypoint = FenixFxAEntryPoint.OnboardingManualSignIn,
)
Navigation.findNavController(view).navigate(directions)
}
}

@ -10,7 +10,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.searchEngines
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.menu.candidate.DrawableMenuIcon
import mozilla.components.concept.menu.candidate.TextMenuCandidate
@ -18,6 +17,7 @@ import mozilla.components.lib.state.helpers.AbstractBinding
import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.R
import org.mozilla.fenix.search.ext.searchEngineShortcuts
import org.mozilla.fenix.search.toolbar.SearchSelectorInteractor
import org.mozilla.fenix.search.toolbar.SearchSelectorMenu
@ -35,7 +35,7 @@ class SearchSelectorMenuBinding(
flow.map { state -> state.search }
.ifChanged()
.collect { search ->
updateSearchSelectorMenu(search.searchEngines)
updateSearchSelectorMenu(search.searchEngineShortcuts)
}
}

@ -11,11 +11,10 @@ import mozilla.components.support.ktx.kotlin.isUrl
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.browser.BrowserAnimator
import org.mozilla.fenix.components.metrics.MetricsUtils
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.home.HomeFragmentDirections
/**
* An interface that handles the view manipulation of the home screen toolbar.
@ -67,21 +66,21 @@ class DefaultToolbarController(
}
}
override fun handlePaste(clipboardText: String) {
val directions = HomeFragmentDirections.actionGlobalSearchDialog(
val directions = NavGraphDirections.actionGlobalSearchDialog(
sessionId = null,
pastedText = clipboardText,
)
navController.nav(R.id.homeFragment, directions)
navController.nav(navController.currentDestination?.id, directions)
}
override fun handleNavigateSearch() {
val directions =
HomeFragmentDirections.actionGlobalSearchDialog(
NavGraphDirections.actionGlobalSearchDialog(
sessionId = null,
)
navController.nav(
R.id.homeFragment,
navController.currentDestination?.id,
directions,
BrowserAnimator.getToolbarNavOptions(activity),
)

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

Loading…
Cancel
Save