Merge tag 'v110.0.1' into fork

fork iceraven-1.21.0
akliuxingyuan 1 year ago
parent ef4366af31
commit e717d79abd

@ -36,4 +36,6 @@ jobs:
type: decision-task
treeherder-symbol: legacy-api-ui
target-tasks-method: legacy_api_ui_tests
when: [] # temporarily unscheduled
when:
- {hour: 11, minute: 0}
- {hour: 20, minute: 0}

@ -37,6 +37,9 @@ messaging:
messages:
type: json
description: A growable collection of messages
notification-config:
type: json
description: Configuration of the notification worker for all notification messages.
on-control:
type: string
description: What should be displayed when a control message is selected.
@ -71,6 +74,14 @@ nimbus-validation:
settings-title:
type: string
description: The title of displayed in the Settings screen and app menu.
pre-permission-notification-prompt:
description: A feature that shows the pre-permission notification prompt.
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "if true, the pre-permission notification prompt is shown to the user."
re-engagement-notification:
description: A feature that shows the re-enagement notification if the user is inactive.
hasExposure: true

@ -3,7 +3,7 @@ on: [pull_request]
jobs:
run-build:
runs-on: ubuntu-20.04
if: github.event.pull_request.head.repo.full_name != github.repository && github.actor != 'MickeyMoz'
if: ${{ false }}
steps:
- name: Checkout repository
uses: actions/checkout@v2
@ -108,7 +108,6 @@ jobs:
run-ui:
runs-on: macos-11
if: ${{ false }}
timeout-minutes: 60
strategy:
matrix:

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: "Update Android-Components"
uses: mozilla-mobile/relbot@master
uses: mozilla-mobile/relbot@5.0.2
if: github.repository == 'mozilla-mobile/fenix'
with:
project: fenix

@ -47,7 +47,7 @@ android {
buildConfigField "String", "GIT_HASH", "\"\"" // see override in release builds for why it's blank.
// This should be the "public" base URL of AMO.
buildConfigField "String", "AMO_BASE_URL", "\"https://addons.mozilla.org\""
buildConfigField "String", "AMO_COLLECTION_NAME", "\"7dfae8669acc4312a65e8ba5553036\""
buildConfigField "String", "AMO_COLLECTION_NAME", "\"Extensions-for-Android\""
buildConfigField "String", "AMO_COLLECTION_USER", "\"mozilla\""
// These add-ons should be excluded for Mozilla Online builds.
buildConfigField "String[]", "MOZILLA_ONLINE_ADDON_EXCLUSIONS",
@ -59,7 +59,8 @@ android {
"\"foxyproxy@eric.h.jung\"," +
"\"{73a6fe31-595d-460b-a920-fcc0f8843232}\"," +
"\"jid1-BoFifL9Vbdl2zQ@jetpack\"," +
"\"woop-NoopscooPsnSXQ@jetpack\"" +
"\"woop-NoopscooPsnSXQ@jetpack\"," +
"\"adnauseam@rednoise.org\"" +
"}"
// This should be the base URL used to call the AMO API.
buildConfigField "String", "AMO_SERVER_URL", "\"https://services.addons.mozilla.org\""
@ -260,7 +261,7 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion = Versions.androidx_compose_compiler
kotlinCompilerExtensionVersion = FenixVersions.androidx_compose_compiler
}
}
@ -506,150 +507,149 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
}
dependencies {
jnaForTest Deps.jna
jnaForTest FenixDependencies.jna
testImplementation files(configurations.jnaForTest.copyRecursive().files)
implementation Deps.mozilla_browser_engine_gecko
implementation Deps.kotlin_stdlib
implementation Deps.kotlin_coroutines
implementation Deps.kotlin_coroutines_android
testImplementation Deps.kotlin_coroutines_test
implementation Deps.androidx_appcompat
implementation Deps.androidx_constraintlayout
implementation Deps.androidx_coordinatorlayout
implementation Deps.google_accompanist_drawablepainter
implementation Deps.google_accompanist_insets
implementation Deps.sentry
implementation Deps.mozilla_compose_awesomebar
implementation Deps.mozilla_concept_awesomebar
implementation Deps.mozilla_concept_base
implementation Deps.mozilla_concept_engine
implementation Deps.mozilla_concept_menu
implementation Deps.mozilla_concept_push
implementation Deps.mozilla_concept_storage
implementation Deps.mozilla_concept_sync
implementation Deps.mozilla_concept_toolbar
implementation Deps.mozilla_concept_tabstray
implementation Deps.mozilla_browser_domains
implementation Deps.mozilla_browser_icons
implementation Deps.mozilla_browser_menu
implementation Deps.mozilla_browser_menu2
implementation Deps.mozilla_browser_session_storage
implementation Deps.mozilla_browser_state
implementation Deps.mozilla_browser_storage_sync
implementation Deps.mozilla_browser_tabstray
implementation Deps.mozilla_browser_thumbnails
implementation Deps.mozilla_browser_toolbar
implementation Deps.mozilla_feature_addons
implementation Deps.mozilla_feature_accounts
implementation Deps.mozilla_feature_app_links
implementation Deps.mozilla_feature_autofill
implementation Deps.mozilla_feature_awesomebar
implementation Deps.mozilla_feature_contextmenu
implementation Deps.mozilla_feature_customtabs
implementation Deps.mozilla_feature_downloads
implementation Deps.mozilla_feature_intent
implementation Deps.mozilla_feature_media
implementation Deps.mozilla_feature_prompts
implementation Deps.mozilla_feature_push
implementation Deps.mozilla_feature_privatemode
implementation Deps.mozilla_feature_pwa
implementation Deps.mozilla_feature_qr
implementation Deps.mozilla_feature_search
implementation Deps.mozilla_feature_session
implementation Deps.mozilla_feature_syncedtabs
implementation Deps.mozilla_feature_toolbar
implementation Deps.mozilla_feature_tabs
implementation Deps.mozilla_feature_findinpage
implementation Deps.mozilla_feature_logins
implementation Deps.mozilla_feature_site_permissions
implementation Deps.mozilla_feature_readerview
implementation Deps.mozilla_feature_tab_collections
implementation Deps.mozilla_feature_recentlyclosed
implementation Deps.mozilla_feature_top_sites
implementation Deps.mozilla_feature_share
implementation Deps.mozilla_feature_accounts_push
implementation Deps.mozilla_feature_webauthn
implementation Deps.mozilla_feature_webcompat
implementation Deps.mozilla_feature_webnotifications
implementation Deps.mozilla_feature_webcompat_reporter
implementation Deps.mozilla_service_pocket
implementation Deps.mozilla_service_contile
implementation Deps.mozilla_service_digitalassetlinks
implementation Deps.mozilla_service_sync_autofill
implementation Deps.mozilla_service_sync_logins
implementation Deps.mozilla_service_firefox_accounts
implementation(Deps.mozilla_service_glean)
implementation Deps.mozilla_service_location
implementation Deps.mozilla_service_nimbus
implementation Deps.mozilla_support_extensions
implementation Deps.mozilla_support_base
implementation Deps.mozilla_support_rusterrors
implementation Deps.mozilla_support_images
implementation Deps.mozilla_support_ktx
implementation Deps.mozilla_support_rustlog
implementation Deps.mozilla_support_utils
implementation Deps.mozilla_support_locale
implementation Deps.mozilla_ui_colors
implementation Deps.mozilla_ui_icons
implementation Deps.mozilla_lib_publicsuffixlist
implementation Deps.mozilla_ui_widgets
implementation Deps.mozilla_ui_tabcounter
implementation Deps.mozilla_lib_crash
implementation Deps.lib_crash_sentry
implementation Deps.mozilla_lib_state
implementation Deps.mozilla_lib_dataprotect
debugImplementation Deps.leakcanary
forkDebugImplementation Deps.leakcanary
implementation Deps.androidx_annotation
implementation Deps.androidx_compose_ui
implementation Deps.androidx_compose_ui_tooling
implementation Deps.androidx_compose_foundation
implementation Deps.androidx_compose_material
implementation Deps.androidx_legacy
implementation Deps.androidx_biometric
implementation Deps.androidx_paging
implementation Deps.androidx_preference
implementation Deps.androidx_fragment
implementation Deps.androidx_navigation_fragment
implementation Deps.androidx_navigation_ui
implementation Deps.androidx_recyclerview
implementation Deps.androidx_lifecycle_common
implementation Deps.androidx_lifecycle_livedata
implementation Deps.androidx_lifecycle_process
implementation Deps.androidx_lifecycle_runtime
implementation Deps.androidx_lifecycle_viewmodel
implementation Deps.androidx_core
implementation Deps.androidx_core_ktx
implementation Deps.androidx_transition
implementation Deps.androidx_work_ktx
implementation Deps.androidx_datastore
implementation Deps.protobuf_javalite
implementation Deps.google_material
androidTestImplementation Deps.uiautomator
implementation FenixDependencies.mozilla_browser_engine_gecko
implementation FenixDependencies.kotlin_stdlib
implementation FenixDependencies.kotlin_coroutines
implementation FenixDependencies.kotlin_coroutines_android
testImplementation FenixDependencies.kotlin_coroutines_test
implementation FenixDependencies.androidx_appcompat
implementation FenixDependencies.androidx_constraintlayout
implementation FenixDependencies.androidx_coordinatorlayout
implementation FenixDependencies.google_accompanist_drawablepainter
implementation FenixDependencies.google_accompanist_insets
implementation FenixDependencies.sentry
implementation FenixDependencies.mozilla_compose_awesomebar
implementation FenixDependencies.mozilla_concept_awesomebar
implementation FenixDependencies.mozilla_concept_base
implementation FenixDependencies.mozilla_concept_engine
implementation FenixDependencies.mozilla_concept_menu
implementation FenixDependencies.mozilla_concept_push
implementation FenixDependencies.mozilla_concept_storage
implementation FenixDependencies.mozilla_concept_sync
implementation FenixDependencies.mozilla_concept_toolbar
implementation FenixDependencies.mozilla_concept_tabstray
implementation FenixDependencies.mozilla_browser_domains
implementation FenixDependencies.mozilla_browser_icons
implementation FenixDependencies.mozilla_browser_menu
implementation FenixDependencies.mozilla_browser_menu2
implementation FenixDependencies.mozilla_browser_session_storage
implementation FenixDependencies.mozilla_browser_state
implementation FenixDependencies.mozilla_browser_storage_sync
implementation FenixDependencies.mozilla_browser_tabstray
implementation FenixDependencies.mozilla_browser_thumbnails
implementation FenixDependencies.mozilla_browser_toolbar
implementation FenixDependencies.mozilla_feature_addons
implementation FenixDependencies.mozilla_feature_accounts
implementation FenixDependencies.mozilla_feature_app_links
implementation FenixDependencies.mozilla_feature_autofill
implementation FenixDependencies.mozilla_feature_awesomebar
implementation FenixDependencies.mozilla_feature_contextmenu
implementation FenixDependencies.mozilla_feature_customtabs
implementation FenixDependencies.mozilla_feature_downloads
implementation FenixDependencies.mozilla_feature_intent
implementation FenixDependencies.mozilla_feature_media
implementation FenixDependencies.mozilla_feature_prompts
implementation FenixDependencies.mozilla_feature_push
implementation FenixDependencies.mozilla_feature_privatemode
implementation FenixDependencies.mozilla_feature_pwa
implementation FenixDependencies.mozilla_feature_qr
implementation FenixDependencies.mozilla_feature_search
implementation FenixDependencies.mozilla_feature_session
implementation FenixDependencies.mozilla_feature_syncedtabs
implementation FenixDependencies.mozilla_feature_toolbar
implementation FenixDependencies.mozilla_feature_tabs
implementation FenixDependencies.mozilla_feature_findinpage
implementation FenixDependencies.mozilla_feature_logins
implementation FenixDependencies.mozilla_feature_site_permissions
implementation FenixDependencies.mozilla_feature_readerview
implementation FenixDependencies.mozilla_feature_tab_collections
implementation FenixDependencies.mozilla_feature_recentlyclosed
implementation FenixDependencies.mozilla_feature_top_sites
implementation FenixDependencies.mozilla_feature_share
implementation FenixDependencies.mozilla_feature_accounts_push
implementation FenixDependencies.mozilla_feature_webauthn
implementation FenixDependencies.mozilla_feature_webcompat
implementation FenixDependencies.mozilla_feature_webnotifications
implementation FenixDependencies.mozilla_feature_webcompat_reporter
implementation FenixDependencies.mozilla_service_pocket
implementation FenixDependencies.mozilla_service_contile
implementation FenixDependencies.mozilla_service_digitalassetlinks
implementation FenixDependencies.mozilla_service_sync_autofill
implementation FenixDependencies.mozilla_service_sync_logins
implementation FenixDependencies.mozilla_service_firefox_accounts
implementation(FenixDependencies.mozilla_service_glean)
implementation FenixDependencies.mozilla_service_location
implementation FenixDependencies.mozilla_service_nimbus
implementation FenixDependencies.mozilla_support_extensions
implementation FenixDependencies.mozilla_support_base
implementation FenixDependencies.mozilla_support_rusterrors
implementation FenixDependencies.mozilla_support_images
implementation FenixDependencies.mozilla_support_ktx
implementation FenixDependencies.mozilla_support_rustlog
implementation FenixDependencies.mozilla_support_utils
implementation FenixDependencies.mozilla_support_locale
implementation FenixDependencies.mozilla_ui_colors
implementation FenixDependencies.mozilla_ui_icons
implementation FenixDependencies.mozilla_lib_publicsuffixlist
implementation FenixDependencies.mozilla_ui_widgets
implementation FenixDependencies.mozilla_ui_tabcounter
implementation FenixDependencies.mozilla_lib_crash
implementation FenixDependencies.lib_crash_sentry
implementation FenixDependencies.mozilla_lib_state
implementation FenixDependencies.mozilla_lib_dataprotect
debugImplementation FenixDependencies.leakcanary
implementation FenixDependencies.androidx_annotation
implementation FenixDependencies.androidx_compose_ui
implementation FenixDependencies.androidx_compose_ui_tooling
implementation FenixDependencies.androidx_compose_foundation
implementation FenixDependencies.androidx_compose_material
implementation FenixDependencies.androidx_legacy
implementation FenixDependencies.androidx_biometric
implementation FenixDependencies.androidx_paging
implementation FenixDependencies.androidx_preference
implementation FenixDependencies.androidx_fragment
implementation FenixDependencies.androidx_navigation_fragment
implementation FenixDependencies.androidx_navigation_ui
implementation FenixDependencies.androidx_recyclerview
implementation FenixDependencies.androidx_lifecycle_common
implementation FenixDependencies.androidx_lifecycle_livedata
implementation FenixDependencies.androidx_lifecycle_process
implementation FenixDependencies.androidx_lifecycle_runtime
implementation FenixDependencies.androidx_lifecycle_viewmodel
implementation FenixDependencies.androidx_core
implementation FenixDependencies.androidx_core_ktx
implementation FenixDependencies.androidx_transition
implementation FenixDependencies.androidx_work_ktx
implementation FenixDependencies.androidx_datastore
implementation FenixDependencies.protobuf_javalite
implementation FenixDependencies.google_material
androidTestImplementation FenixDependencies.uiautomator
androidTestImplementation "tools.fastlane:screengrab:2.0.0"
// This Falcon version is added to maven central now required for Screengrab
implementation 'com.jraska:falcon:2.2.0'
androidTestImplementation Deps.androidx_compose_ui_test
androidTestImplementation FenixDependencies.androidx_compose_ui_test
androidTestImplementation Deps.espresso_core, {
androidTestImplementation FenixDependencies.espresso_core, {
exclude group: 'com.android.support', module: 'support-annotations'
}
androidTestImplementation(Deps.espresso_contrib) {
androidTestImplementation(FenixDependencies.espresso_contrib) {
exclude module: 'appcompat-v7'
exclude module: 'support-v4'
exclude module: 'support-annotations'
@ -659,36 +659,36 @@ dependencies {
exclude module: 'protobuf-lite'
}
androidTestImplementation Deps.androidx_test_core
androidTestImplementation Deps.espresso_idling_resources
androidTestImplementation Deps.espresso_intents
androidTestImplementation FenixDependencies.androidx_test_core
androidTestImplementation FenixDependencies.espresso_idling_resources
androidTestImplementation FenixDependencies.espresso_intents
androidTestImplementation Deps.tools_test_runner
androidTestImplementation Deps.tools_test_rules
androidTestUtil Deps.orchestrator
androidTestImplementation Deps.espresso_core, {
androidTestImplementation FenixDependencies.tools_test_runner
androidTestImplementation FenixDependencies.tools_test_rules
androidTestUtil FenixDependencies.orchestrator
androidTestImplementation FenixDependencies.espresso_core, {
exclude group: 'com.android.support', module: 'support-annotations'
}
androidTestImplementation Deps.androidx_junit
androidTestImplementation Deps.androidx_test_extensions
androidTestImplementation Deps.androidx_work_testing
androidTestImplementation Deps.androidx_benchmark_junit4
androidTestImplementation Deps.mockwebserver
testImplementation Deps.mozilla_support_test
testImplementation Deps.mozilla_support_test_libstate
testImplementation Deps.androidx_junit
testImplementation Deps.androidx_test_extensions
testImplementation Deps.androidx_work_testing
testImplementation (Deps.robolectric) {
androidTestImplementation FenixDependencies.androidx_junit
androidTestImplementation FenixDependencies.androidx_test_extensions
androidTestImplementation FenixDependencies.androidx_work_testing
androidTestImplementation FenixDependencies.androidx_benchmark_junit4
androidTestImplementation FenixDependencies.mockwebserver
testImplementation FenixDependencies.mozilla_support_test
testImplementation FenixDependencies.mozilla_support_test_libstate
testImplementation FenixDependencies.androidx_junit
testImplementation FenixDependencies.androidx_test_extensions
testImplementation FenixDependencies.androidx_work_testing
testImplementation (FenixDependencies.robolectric) {
exclude group: 'org.apache.maven'
}
testImplementation 'org.apache.maven:maven-ant-tasks:2.1.3'
implementation Deps.mozilla_support_rusthttp
implementation FenixDependencies.mozilla_support_rusthttp
androidTestImplementation Deps.mockk_android
testImplementation Deps.mockk
androidTestImplementation FenixDependencies.mockk_android
testImplementation FenixDependencies.mockk
// For the initial release of Glean 19, we require consumer applications to
// depend on a separate library for unit tests. This will be removed in future releases.
@ -702,9 +702,9 @@ protobuf {
// See https://github.com/mozilla-mobile/fenix/issues/22321
protoc {
if (osdetector.os == "osx") {
artifact = "${Deps.protobuf_compiler}:osx-x86_64"
artifact = "${FenixDependencies.protobuf_compiler}:osx-x86_64"
} else {
artifact = Deps.protobuf_compiler
artifact = FenixDependencies.protobuf_compiler
}
}

File diff suppressed because it is too large Load Diff

@ -13,31 +13,31 @@
<h3>Level 1 (Basic) List</h3>
<p>social-track-digest256:</p>
<img
src="https://social-track-digest256.dummytracker.org/test_not_blocked.png"
onerror="this.onerror=null;this.src='https://not-a-tracker.dummytracker.org/test_blocked.png'">
src="https://social-track-digest256.dummytracker.org/test_not_blocked.png" alt="social not blocked"
onerror="this.onerror=null;this.src='https://not-a-tracker.dummytracker.org/test_blocked.png';this.alt='social blocked'">
<br/>
<p>ads-track-digest256:</p>
<img
src="https://ads-track-digest256.dummytracker.org/test_not_blocked.png"
onerror="this.onerror=null;this.src='https://not-a-tracker.dummytracker.org/test_blocked.png'">
onerror="this.onerror=null;this.src='https://not-a-tracker.dummytracker.org/test_blocked.png';this.alt='ads blocked'">
<br/>
<p>analytics-track-digest256:</p>
<img
src="https://analytics-track-digest256.dummytracker.org/test_not_blocked.png"
onerror="this.onerror=null;this.src='https://not-a-tracker.dummytracker.org/test_blocked.png'">
onerror="this.onerror=null;this.src='https://not-a-tracker.dummytracker.org/test_blocked.png';this.alt='analytics blocked'">
<br/>
<p>Fingerprinting:
<pre id="result">test not run</pre>
<script src="https://base-fingerprinting-track-digest256.dummytracker.org/tracker.js"
onerror="this.onerror=null;var result=document.getElementById('result');result.innerHTML='blocked';"
onload="this.onload=null;var result=document.getElementById('result');result.innerHTML='NOT blocked';"
onerror="this.onerror=null;var result=document.getElementById('result');result.innerHTML='Fingerprinting blocked';"
onload="this.onload=null;var result=document.getElementById('result');result.innerHTML='Fingerprinting NOT blocked';"
></script>
</p>
<br/>
<p>Cryptomining:
<img
src="https://base-cryptomining-track-digest256.dummytracker.org/test_not_blocked.png" alt="not blocked"
onerror="this.onerror=null;this.src='https://not-a-tracker.dummytracker.org/test_blocked.png';this.alt='blocked'">
src="https://base-cryptomining-track-digest256.dummytracker.org/test_not_blocked.png" alt="Cryptomining not blocked"
onerror="this.onerror=null;this.src='https://not-a-tracker.dummytracker.org/test_blocked.png';this.alt='Cryptomining blocked'">
</p>
<p><b>Cookie blocking</b>

@ -28,6 +28,7 @@ import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@ -143,6 +144,7 @@ class BaselinePingTest {
return null
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1807288")
@Test
fun validateBaselinePing() {
// Wait for the app to be idle/ready.

@ -4,6 +4,8 @@
package org.mozilla.fenix.helpers
import org.mozilla.fenix.helpers.TestHelper.getSponsoredShortcutTitle
object Constants {
// Device or AVD requires a Google Services Android OS installation
@ -28,4 +30,18 @@ object Constants {
"Bing" to "firefox&pc=MOZB&form=MOZMBA",
"DuckDuckGo" to "t=fpas",
)
val firstSponsoredShortcutTitle by lazy { getSponsoredShortcutTitle(2) }
val secondSponsoredShortcutTitle by lazy { getSponsoredShortcutTitle(3) }
// Expected for en-us defaults
val defaultTopSitesList by lazy {
mapOf(
"Google" to "Google",
"First sponsored shortcut" to firstSponsoredShortcutTitle,
"Second sponsored shortcut" to secondSponsoredShortcutTitle,
"Top Articles" to "Top Articles",
"Wikipedia" to "Wikipedia",
)
}
}

@ -0,0 +1,85 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.helpers
import androidx.test.uiautomator.UiObject
import androidx.test.uiautomator.UiSelector
import org.junit.Assert.assertTrue
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.mDevice
/**
* Helper for querying and interacting with items based on their matchers.
*/
object MatcherHelper {
fun itemWithResId(resourceId: String) =
mDevice.findObject(UiSelector().resourceId(resourceId))
fun itemContainingText(itemText: String) =
mDevice.findObject(UiSelector().textContains(itemText))
fun itemWithDescription(description: String) =
mDevice.findObject(UiSelector().descriptionContains(description))
fun checkedItemWithResId(resourceId: String, isChecked: Boolean) =
mDevice.findObject(UiSelector().resourceId(resourceId).checked(isChecked))
fun checkedItemWithResIdAndText(resourceId: String, text: String, isChecked: Boolean) =
mDevice.findObject(
UiSelector()
.resourceId(resourceId)
.textContains(text)
.checked(isChecked),
)
fun itemWithResIdAndDescription(resourceId: String, description: String) =
mDevice.findObject(UiSelector().resourceId(resourceId).descriptionContains(description))
fun itemWithResIdAndText(resourceId: String, text: String) =
mDevice.findObject(UiSelector().resourceId(resourceId).text(text))
fun assertItemWithResIdExists(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
}
fun assertItemContainingTextExists(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
}
fun assertItemWithDescriptionExists(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
}
fun assertCheckedItemWithResIdExists(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
}
fun assertCheckedItemWithResIdAndTextExists(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
}
fun assertItemWithResIdAndDescriptionExists(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
}
fun assertItemWithResIdAndTextExists(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
}
}

@ -445,4 +445,25 @@ object TestHelper {
clipBoard.setPrimaryClip(clipData)
}
/**
* Returns sponsored shortcut title based on the index.
*/
fun getSponsoredShortcutTitle(position: Int): String {
val sponsoredShortcut = mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/top_site_item")
.index(position - 1),
).getChild(
UiSelector()
.resourceId("$packageName:id/top_site_title"),
).text
return sponsoredShortcut
}
fun verifyLightThemeApplied(expected: Boolean) =
assertFalse("Light theme not selected", expected)
fun verifyDarkThemeApplied(expected: Boolean) = assertTrue("Dark theme not selected", expected)
}

@ -17,7 +17,6 @@ import androidx.test.uiautomator.Until
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.R
@ -164,7 +163,6 @@ class MenuScreenShotTest : ScreenshotTest() {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun tabMenuTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {

@ -36,16 +36,18 @@ class CollectionTest {
private val collectionName = "First Collection"
@get:Rule
val composeTestRule = AndroidComposeTestRule(
// disabling these features to have better visibility of Collections,
// and to avoid multiple matches on tab items
HomeActivityIntentTestRule(
isPocketEnabled = false,
isJumpBackInCFREnabled = false,
isRecentTabsFeatureEnabled = false,
isRecentlyVisitedFeatureEnabled = false,
),
) { it.activity }
val composeTestRule =
AndroidComposeTestRule(
HomeActivityIntentTestRule(
isHomeOnboardingDialogEnabled = false,
isJumpBackInCFREnabled = false,
isRecentTabsFeatureEnabled = false,
isRecentlyVisitedFeatureEnabled = false,
isPocketEnabled = false,
isWallpaperOnboardingEnabled = false,
isTCPCFREnabled = false,
),
) { it.activity }
@Before
fun setUp() {
@ -63,7 +65,6 @@ class CollectionTest {
@SmokeTest
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun createFirstCollectionTest() {
val firstWebPage = getGenericAsset(mockWebServer, 1)
val secondWebPage = getGenericAsset(mockWebServer, 2)
@ -76,7 +77,6 @@ class CollectionTest {
}.submitQuery(secondWebPage.url.toString()) {
mDevice.waitForIdle()
}.goToHomescreen {
swipeToBottom()
}.clickSaveTabsToCollectionButton {
longClickTab(firstWebPage.title)
selectTab(secondWebPage.title, numOfTabs = 2)
@ -96,7 +96,6 @@ class CollectionTest {
@SmokeTest
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun verifyExpandedCollectionItemsTest() {
val webPage = getGenericAsset(mockWebServer, 1)
val webPageUrl = webPage.url.host.toString()
@ -146,7 +145,6 @@ class CollectionTest {
@SmokeTest
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun openAllTabsInCollectionTest() {
val firstTestPage = getGenericAsset(mockWebServer, 1)
val secondTestPage = getGenericAsset(mockWebServer, 2)
@ -179,7 +177,6 @@ class CollectionTest {
@SmokeTest
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun shareCollectionTest() {
val firstWebsite = getGenericAsset(mockWebServer, 1)
val secondWebsite = getGenericAsset(mockWebServer, 2)
@ -204,11 +201,10 @@ class CollectionTest {
}
}
@SmokeTest
@Test
// Test running on beta/release builds in CI:
// caution when making changes to it, so they don't block the builds
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
@SmokeTest
@Test
fun deleteCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)
@ -231,9 +227,8 @@ class CollectionTest {
}
}
@Test
// open a webpage, and add currently opened tab to existing collection
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
@Test
fun mainMenuSaveToExistingCollection() {
val firstWebPage = getGenericAsset(mockWebServer, 1)
val secondWebPage = getGenericAsset(mockWebServer, 2)
@ -260,7 +255,6 @@ class CollectionTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun verifyAddTabButtonOfCollectionMenu() {
val firstWebPage = getGenericAsset(mockWebServer, 1)
val secondWebPage = getGenericAsset(mockWebServer, 2)
@ -287,7 +281,6 @@ class CollectionTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun renameCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)
@ -308,7 +301,6 @@ class CollectionTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun createSecondCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)
@ -331,7 +323,6 @@ class CollectionTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun removeTabFromCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)
@ -354,7 +345,6 @@ class CollectionTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun swipeLeftToRemoveTabFromCollectionTest() {
val testPage = getGenericAsset(mockWebServer, 1)
@ -381,7 +371,6 @@ class CollectionTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun swipeRightToRemoveTabFromCollectionTest() {
val testPage = getGenericAsset(mockWebServer, 1)
@ -493,7 +482,7 @@ class CollectionTest {
homeScreen {
verifySnackBarText("Collection deleted")
clickUndoCollectionDeletion("UNDO")
clickUndoSnackBarButton()
verifyCollectionIsDisplayed(collectionName, true)
}
}

@ -33,7 +33,7 @@ class DownloadFileTypesTest(fileName: String) {
@JvmStatic
@Parameterized.Parameters
fun downloadList() = listOf(
"washington.pdf",
"smallZip.zip",
"MyDocument.docx",
"audioSample.mp3",
"textfile.txt",

@ -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
@ -165,7 +164,6 @@ class EnhancedTrackingProtectionTest {
}
}
@Ignore("Permanent failure: https://github.com/mozilla-mobile/fenix/issues/27312")
@Test
fun testStrictVisitSheetDetails() {
appContext.settings().setStrictETP()
@ -182,7 +180,11 @@ class EnhancedTrackingProtectionTest {
navigationToolbar {
}.enterURLAndEnterToBrowser(trackingProtectionTest.url) {
verifyTrackingProtectionWebContent("blocked")
verifyTrackingProtectionWebContent("social blocked")
verifyTrackingProtectionWebContent("ads blocked")
verifyTrackingProtectionWebContent("analytics blocked")
verifyTrackingProtectionWebContent("Fingerprinting blocked")
verifyTrackingProtectionWebContent("Cryptomining blocked")
}
enhancedTrackingProtection {
}.openEnhancedTrackingProtectionSheet {
@ -197,7 +199,6 @@ class EnhancedTrackingProtectionTest {
}
}
@Ignore("Permanent failure: https://github.com/mozilla-mobile/fenix/issues/27312")
@SmokeTest
@Test
fun customTrackingProtectionSettingsTest() {
@ -217,7 +218,13 @@ class EnhancedTrackingProtectionTest {
// browsing a basic page to allow GV to load on a fresh run
}.enterURLAndEnterToBrowser(genericWebPage.url) {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(trackingPage.url) {}
}.enterURLAndEnterToBrowser(trackingPage.url) {
verifyTrackingProtectionWebContent("social blocked")
verifyTrackingProtectionWebContent("ads blocked")
verifyTrackingProtectionWebContent("analytics blocked")
verifyTrackingProtectionWebContent("Fingerprinting blocked")
verifyTrackingProtectionWebContent("Cryptomining blocked")
}
enhancedTrackingProtection {
}.openEnhancedTrackingProtectionSheet {

@ -155,81 +155,6 @@ class HomeScreenTest {
}
}
@Test
fun dismissOnboardingUsingSettingsTest() {
homeScreen {
verifyWelcomeHeader()
}.openThreeDotMenu {
}.openSettings {
verifyGeneralHeading()
}.goBack {
verifyExistingTopSitesList()
}
}
@Test
fun dismissOnboardingUsingBookmarksTest() {
homeScreen {
verifyWelcomeHeader()
}.openThreeDotMenu {
}.openBookmarks {
verifyBookmarksMenuView()
navigateUp()
}
homeScreen {
verifyExistingTopSitesList()
}
}
@Test
fun dismissOnboardingUsingHelpTest() {
activityTestRule.activityRule.applySettingsExceptions {
it.isJumpBackInCFREnabled = false
it.isWallpaperOnboardingEnabled = false
}
homeScreen {
verifyWelcomeHeader()
}.openThreeDotMenu {
}.openHelp {
verifyHelpUrl()
}.goBack {
verifyExistingTopSitesList()
}
}
@Test
fun dismissOnboardingWithPageLoadTest() {
activityTestRule.activityRule.applySettingsExceptions {
it.isJumpBackInCFREnabled = false
it.isWallpaperOnboardingEnabled = false
}
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
verifyWelcomeHeader()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.goToHomescreen {
verifyExistingTopSitesList()
}
}
@Test
fun toolbarTapDoesntDismissOnboardingTest() {
homeScreen {
verifyWelcomeHeader()
}.openSearch {
verifyScanButton()
verifySearchEngineButton()
verifyKeyboardVisibility()
}.dismissSearchBar {
verifyWelcomeHeader()
}
}
@Test
fun verifyPocketHomepageStoriesTest() {
activityTestRule.activityRule.applySettingsExceptions {

@ -65,7 +65,7 @@ class NavigationToolbarTest {
}.enterURLAndEnterToBrowser(nextWebPage.url) {
verifyUrl(nextWebPage.url.toString())
}.openThreeDotMenu {
}.goBack {
}.goToPreviousPage {
mDevice.waitForIdle()
verifyUrl(defaultWebPage.url.toString())
}
@ -84,7 +84,7 @@ class NavigationToolbarTest {
mDevice.waitForIdle()
verifyUrl(nextWebPage.url.toString())
}.openThreeDotMenu {
}.goBack {
}.goToPreviousPage {
mDevice.waitForIdle()
verifyUrl(defaultWebPage.url.toString())
}

@ -0,0 +1,223 @@
package org.mozilla.fenix.ui
import android.content.res.Configuration
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.verifyDarkThemeApplied
import org.mozilla.fenix.helpers.TestHelper.verifyLightThemeApplied
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
class OnboardingTest {
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer
private val privacyNoticeLink = "mozilla.org/en-US/privacy/firefox"
@get:Rule
val activityTestRule = HomeActivityTestRule.withDefaultSettingsOverrides()
@Before
fun setUp() {
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
private fun getUITheme(): Boolean {
val mode =
activityTestRule.activity.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK)
return when (mode) {
Configuration.UI_MODE_NIGHT_YES -> true // dark theme is set
Configuration.UI_MODE_NIGHT_NO -> false // dark theme is not set, using light theme
else -> false // default option is light theme
}
}
// Verifies the first run onboarding screen
@SmokeTest
@Test
fun firstRunScreenTest() {
homeScreen {
verifyHomeScreenAppBarItems()
verifyHomeScreenWelcomeItems()
verifyChooseYourThemeCard(
isDarkThemeChecked = false,
isLightThemeChecked = false,
isAutomaticThemeChecked = true,
)
verifyToolbarPlacementCard(isBottomChecked = true, isTopChecked = false)
verifySignInToSyncCard()
verifyPrivacyProtectionCard(isStandardChecked = true, isStrictChecked = false)
verifyPrivacyNoticeCard()
verifyStartBrowsingSection()
verifyNavigationToolbarItems("0")
}
}
// Verifies the functionality of the onboarding Start Browsing button
@SmokeTest
@Test
fun startBrowsingButtonTest() {
homeScreen {
verifyStartBrowsingButton()
}.clickStartBrowsingButton {
verifySearchView()
}
}
@Test
fun dismissOnboardingUsingSettingsTest() {
homeScreen {
verifyWelcomeHeader()
}.openThreeDotMenu {
}.openSettings {
verifyGeneralHeading()
}.goBack {
verifyExistingTopSitesList()
}
}
@Test
fun dismissOnboardingUsingBookmarksTest() {
homeScreen {
verifyWelcomeHeader()
}.openThreeDotMenu {
}.openBookmarks {
verifyBookmarksMenuView()
navigateUp()
}
homeScreen {
verifyExistingTopSitesList()
}
}
@Test
fun dismissOnboardingUsingHelpTest() {
homeScreen {
verifyWelcomeHeader()
}.openThreeDotMenu {
}.openHelp {
verifyHelpUrl()
}.goBack {
verifyExistingTopSitesList()
}
}
@Test
fun toolbarTapDoesntDismissOnboardingTest() {
homeScreen {
verifyWelcomeHeader()
}.openSearch {
verifyScanButton()
verifySearchEngineButton()
verifyKeyboardVisibility()
}.dismissSearchBar {
verifyWelcomeHeader()
}
}
@Test
fun dismissOnboardingWithPageLoadTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
verifyWelcomeHeader()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.goToHomescreen {
verifyExistingTopSitesList()
}
}
@Test
fun chooseYourThemeCardTest() {
homeScreen {
verifyChooseYourThemeCard(
isDarkThemeChecked = false,
isLightThemeChecked = false,
isAutomaticThemeChecked = true,
)
clickLightThemeButton()
verifyChooseYourThemeCard(
isDarkThemeChecked = false,
isLightThemeChecked = true,
isAutomaticThemeChecked = false,
)
verifyLightThemeApplied(getUITheme())
clickDarkThemeButton()
verifyChooseYourThemeCard(
isDarkThemeChecked = true,
isLightThemeChecked = false,
isAutomaticThemeChecked = false,
)
verifyDarkThemeApplied(getUITheme())
clickAutomaticThemeButton()
verifyChooseYourThemeCard(
isDarkThemeChecked = false,
isLightThemeChecked = false,
isAutomaticThemeChecked = true,
)
verifyLightThemeApplied(getUITheme())
}
}
@Test
fun pickYourToolbarPlacementCardTest() {
homeScreen {
verifyToolbarPlacementCard(isBottomChecked = true, isTopChecked = false)
clickTopToolbarPlacementButton()
verifyToolbarPosition(defaultPosition = false)
clickBottomToolbarPlacementButton()
verifyToolbarPosition(defaultPosition = true)
}
}
@Test
fun privacyProtectionByDefaultCardTest() {
homeScreen {
verifyPrivacyProtectionCard(isStandardChecked = true, isStrictChecked = false)
clickStrictTrackingProtectionButton()
verifyPrivacyProtectionCard(isStandardChecked = false, isStrictChecked = true)
clickStandardTrackingProtectionButton()
verifyPrivacyProtectionCard(isStandardChecked = true, isStrictChecked = false)
}
}
@Test
fun pickUpWhereYouLeftOffCardTest() {
homeScreen {
verifySignInToSyncCard()
}.clickSignInButton {
verifyTurnOnSyncMenu()
}
}
@Test
fun youControlYourDataCardTest() {
homeScreen {
verifyPrivacyNoticeCard()
}.clickPrivacyNoticeButton {
verifyUrl(privacyNoticeLink)
}.goBack {
verifyPrivacyNoticeCard()
}
}
}

@ -1,6 +1,7 @@
package org.mozilla.fenix.ui
import androidx.core.net.toUri
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
@ -45,6 +46,7 @@ class PwaTest {
}
}
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/28212")
@SmokeTest
@Test
fun emailLinkPWATest() {

@ -141,7 +141,7 @@ class ReaderViewTest {
verifyReaderViewDetected(true)
}.openThreeDotMenu {
verifyReaderViewAppearance(false)
}.close { }
}.closeBrowserMenuToBrowser { }
}
@Test

@ -10,6 +10,8 @@ import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.exitMenu
import org.mozilla.fenix.helpers.TestHelper.verifyDarkThemeApplied
import org.mozilla.fenix.helpers.TestHelper.verifyLightThemeApplied
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar

@ -17,6 +17,7 @@ import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
@ -53,6 +54,7 @@ class SettingsPrivacyTest {
@Before
fun setUp() {
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
appContext.settings().userOptOutOfReEngageCookieBannerDialog = true
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()

@ -98,53 +98,81 @@ class SettingsSearchTest {
@Test
fun toggleSearchBookmarksAndHistoryTest() {
// Bookmarks 2 websites, toggles the bookmarks and history search settings off,
// then verifies if the websites do not show in the suggestions.
val page1 = getGenericAsset(mockWebServer, 1)
val page2 = getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(page1.url) {
}.openTabDrawer {
closeTab()
}
homeScreen {
}.openSearch {
typeSearch("test")
verifyFirefoxSuggestResults(
activityTestRule,
"test",
"Firefox Suggest",
"Test_Page_1",
)
}.clickSearchSuggestion("Test_Page_1") {
verifyUrl(page1.url.toString())
}.openThreeDotMenu {
}.bookmarkPage {
}.openTabDrawer {
closeTab()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(page2.url) {
verifyUrl(page2.url.toString())
}.openThreeDotMenu {
}.bookmarkPage {
}.openTabDrawer {
closeTab()
}
// Verifies that bookmarks & history suggestions are shown
homeScreen {
}.openThreeDotMenu {
}.openHistory {
verifyHistoryListExists()
clickDeleteHistoryButton("Test_Page_2")
}
exitMenu()
homeScreen {
}.openSearch {
typeSearch("test")
expandSearchSuggestionsList()
verifyFirefoxSuggestResults(activityTestRule, "Firefox Suggest")
verifyFirefoxSuggestResults(activityTestRule, "Test_Page_1")
verifyFirefoxSuggestResults(activityTestRule, "Test_Page_2")
}.dismissSearchBar {
verifyFirefoxSuggestResults(
activityTestRule,
"test",
"Firefox Suggest",
"Test_Page_2",
)
}.clickSearchSuggestion("Test_Page_2") {
verifyUrl(page2.url.toString())
}.openTabDrawer {
closeTab()
}
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
// Disables the search bookmarks & history settings
verifySearchBookmarks()
switchSearchBookmarksToggle()
switchSearchHistoryToggle()
exitMenu()
switchSearchBookmarksToggle()
}
// Verifies that bookmarks and history suggestions are not shown
exitMenu()
homeScreen {
}.openSearch {
typeSearch("test")
expandSearchSuggestionsList()
verifyNoSuggestionsAreDisplayed(activityTestRule, "Firefox Suggest")
verifyNoSuggestionsAreDisplayed(activityTestRule, "Test_Page_1")
verifyNoSuggestionsAreDisplayed(activityTestRule, "Test_Page_2")
verifyNoSuggestionsAreDisplayed(
activityTestRule,
"Firefox Suggest",
"Test_Page_1",
"Test_Page_2",
)
}
}
@ -453,6 +481,7 @@ class SettingsSearchTest {
"eBay",
"Wikipedia",
)
scrollToSearchEngineSettings(activityTestRule)
}.clickSearchEngineSettings(activityTestRule) {
toggleShowSearchShortcuts()
verifyShowSearchEnginesToggleState(false)

@ -17,7 +17,6 @@ import mozilla.components.concept.engine.mediasession.MediaSession
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.IntentReceiverActivity
@ -106,36 +105,6 @@ class SmokeTest {
featureSettingsHelper.resetAllFeatureFlags()
}
// Verifies the first run onboarding screen
@Test
fun firstRunScreenTest() {
homeScreen {
verifyHomeScreenAppBarItems()
verifyHomeScreenWelcomeItems()
verifyChooseYourThemeCard(
isDarkThemeChecked = false,
isLightThemeChecked = false,
isAutomaticThemeChecked = true,
)
verifyToolbarPlacementCard(isBottomChecked = true, isTopChecked = false)
verifySignInToSyncCard()
verifyPrivacyProtectionCard(isStandardChecked = true, isStrictChecked = false)
verifyPrivacyNoticeCard()
verifyStartBrowsingSection()
verifyNavigationToolbarItems("0")
}
}
// Verifies the functionality of the onboarding Start Browsing button
@Test
fun startBrowsingButtonTest() {
homeScreen {
verifyStartBrowsingButton()
}.clickStartBrowsingButton {
verifySearchView()
}
}
/* Verifies the nav bar:
- opening a web page
- the existence of nav bar items
@ -172,7 +141,7 @@ class SmokeTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
waitForPageToLoad()
}.openThreeDotMenu {
verifyPageThreeDotMainMenuItems()
verifyPageThreeDotMainMenuItems(isRequestDesktopSiteEnabled = false)
}
}
@ -283,7 +252,7 @@ class SmokeTest {
}.openThreeDotMenu {
expandMenu()
}.openAddToHomeScreen {
verifyShortcutNameField("Test_Page_1")
verifyShortcutTextFieldTitle("Test_Page_1")
addShortcutName(shortcutTitle)
clickAddShortcutButton()
clickAddAutomaticallyButton()
@ -524,7 +493,6 @@ class SmokeTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun shareTabsFromTabsTrayTest() {
val firstWebsite = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebsite = TestAssetHelper.getGenericAsset(mockWebServer, 2)
@ -572,7 +540,6 @@ class SmokeTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun privateTabsTrayWithOpenedTabTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -582,7 +549,6 @@ class SmokeTest {
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
mDevice.waitForIdle()
}.openTabDrawer {
verifyNormalBrowsingButtonIsSelected(false)
verifyPrivateBrowsingButtonIsSelected(true)
@ -592,7 +558,8 @@ class SmokeTest {
verifyExistingTabList()
verifyExistingOpenTabs(website.title)
verifyCloseTabsButton(website.title)
verifyOpenedTabThumbnail()
// Disabled step due to ongoing tabs tray compose refactoring, see: https://github.com/mozilla-mobile/fenix/issues/21318
// verifyOpenedTabThumbnail()
verifyPrivateBrowsingNewTabButton()
}
}
@ -772,7 +739,6 @@ class SmokeTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun tabMediaControlButtonTest() {
val audioTestPage = TestAssetHelper.getAudioPageAsset(mockWebServer)

@ -0,0 +1,252 @@
/* 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.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.Constants.defaultTopSitesList
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.exitMenu
import org.mozilla.fenix.helpers.TestHelper.getSponsoredShortcutTitle
import org.mozilla.fenix.ui.robots.homeScreen
/**
* Tests Sponsored shortcuts functionality
*/
class SponsoredShortcutsTest {
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer
private val defaultSearchEngine = "Amazon.com"
private lateinit var sponsoredShortcutTitle: String
private lateinit var sponsoredShortcutTitle2: String
@get:Rule
val activityIntentTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
@Before
fun setUp() {
TestHelper.appContext.settings().userOptOutOfReEngageCookieBannerDialog = true
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
// Expected for en-us defaults
@SmokeTest
@Test
fun verifySponsoredShortcutsListTest() {
homeScreen {
defaultTopSitesList.values.forEach { value ->
verifyExistingTopSitesTabs(value)
}
}.openThreeDotMenu {
}.openCustomizeHome {
verifySponsoredShortcutsCheckBox(true)
clickSponsoredShortcuts()
verifySponsoredShortcutsCheckBox(false)
}.goBack {
verifyNotExistingSponsoredTopSitesList()
}
}
@Test
fun openSponsoredShortcutTest() {
homeScreen {
sponsoredShortcutTitle = getSponsoredShortcutTitle(2)
}.openSponsoredShortcut(sponsoredShortcutTitle) {
verifyUrl(sponsoredShortcutTitle)
}
}
@Test
fun openSponsoredShortcutInPrivateBrowsingTest() {
homeScreen {
sponsoredShortcutTitle = getSponsoredShortcutTitle(2)
}.openContextMenuOnSponsoredShortcut(sponsoredShortcutTitle) {
}.openTopSiteInPrivateTab {
verifyUrl(sponsoredShortcutTitle)
}
}
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/25926")
@Test
fun verifySponsorsAndPrivacyLinkTest() {
homeScreen {
sponsoredShortcutTitle = getSponsoredShortcutTitle(2)
}.openContextMenuOnSponsoredShortcut(sponsoredShortcutTitle) {
}.clickSponsorsAndPrivacyButton {
verifyUrl("support.mozilla.org/en-US/kb/sponsor-privacy")
}
}
@Test
fun verifySponsoredShortcutsSettingsOptionTest() {
homeScreen {
sponsoredShortcutTitle = getSponsoredShortcutTitle(2)
}.openContextMenuOnSponsoredShortcut(sponsoredShortcutTitle) {
}.clickSponsoredShortcutsSettingsButton {
verifyHomePageView()
}
}
@Test
fun verifySponsoredShortcutsDetailsTest() {
homeScreen {
sponsoredShortcutTitle = getSponsoredShortcutTitle(2)
sponsoredShortcutTitle2 = getSponsoredShortcutTitle(3)
verifySponsoredShortcutDetails(sponsoredShortcutTitle, 2)
verifySponsoredShortcutDetails(sponsoredShortcutTitle2, 3)
}
}
// The default search engine should not be displayed as a sponsored shortcut
@Test
fun defaultSearchEngineIsNotDisplayedAsSponsoredShortcutTest() {
val sponsoredShortcutTitle = "Amazon"
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
changeDefaultSearchEngine(defaultSearchEngine)
}
exitMenu()
homeScreen {
verifySponsoredShortcutDoesNotExist(sponsoredShortcutTitle, 2)
verifySponsoredShortcutDoesNotExist(sponsoredShortcutTitle, 3)
}
}
// 1 sponsored shortcut should be displayed if there are 7 pinned top sites
@Test
fun verifySponsoredShortcutsListWithSevenPinnedSitesTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
val thirdWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 3)
val fourthWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 4)
homeScreen {
sponsoredShortcutTitle = getSponsoredShortcutTitle(2)
sponsoredShortcutTitle2 = getSponsoredShortcutTitle(3)
verifySponsoredShortcutDetails(sponsoredShortcutTitle, 2)
verifySponsoredShortcutDetails(sponsoredShortcutTitle2, 3)
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
verifyPageContent(firstWebPage.content)
}.openThreeDotMenu {
expandMenu()
}.addToFirefoxHome {
}.goToHomescreen {
verifyExistingTopSitesTabs(firstWebPage.title)
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(secondWebPage.url) {
verifyPageContent(secondWebPage.content)
}.openThreeDotMenu {
expandMenu()
}.addToFirefoxHome {
}.goToHomescreen {
verifyExistingTopSitesTabs(secondWebPage.title)
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(thirdWebPage.url) {
verifyPageContent(thirdWebPage.content)
}.openThreeDotMenu {
expandMenu()
}.addToFirefoxHome {
}.goToHomescreen {
verifyExistingTopSitesTabs(thirdWebPage.title)
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(fourthWebPage.url) {
verifyPageContent(fourthWebPage.content)
}.openThreeDotMenu {
expandMenu()
}.addToFirefoxHome {
}.goToHomescreen {
verifySponsoredShortcutDetails(sponsoredShortcutTitle, 2)
verifySponsoredShortcutDoesNotExist(sponsoredShortcutTitle2, 3)
}
}
// No sponsored shortcuts should be displayed if there are 8 pinned top sites
@Test
fun verifySponsoredShortcutsListWithEightPinnedSitesTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
val thirdWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 3)
val fourthWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 4)
val fifthWebPage = TestAssetHelper.getLoremIpsumAsset(mockWebServer)
homeScreen {
sponsoredShortcutTitle = getSponsoredShortcutTitle(2)
sponsoredShortcutTitle2 = getSponsoredShortcutTitle(3)
verifySponsoredShortcutDetails(sponsoredShortcutTitle, 2)
verifySponsoredShortcutDetails(sponsoredShortcutTitle2, 3)
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
verifyPageContent(firstWebPage.content)
}.openThreeDotMenu {
expandMenu()
}.addToFirefoxHome {
}.goToHomescreen {
verifyExistingTopSitesTabs(firstWebPage.title)
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(secondWebPage.url) {
verifyPageContent(secondWebPage.content)
}.openThreeDotMenu {
expandMenu()
}.addToFirefoxHome {
}.goToHomescreen {
verifyExistingTopSitesTabs(secondWebPage.title)
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(thirdWebPage.url) {
verifyPageContent(thirdWebPage.content)
}.openThreeDotMenu {
expandMenu()
}.addToFirefoxHome {
}.goToHomescreen {
verifyExistingTopSitesTabs(thirdWebPage.title)
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(fourthWebPage.url) {
verifyPageContent(fourthWebPage.content)
}.openThreeDotMenu {
expandMenu()
}.addToFirefoxHome {
}.goToHomescreen {
verifyExistingTopSitesTabs(fourthWebPage.title)
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(fifthWebPage.url) {
verifyPageContent(fifthWebPage.content)
}.openThreeDotMenu {
expandMenu()
}.addToFirefoxHome {
}.goToHomescreen {
verifySponsoredShortcutDoesNotExist(sponsoredShortcutTitle, 2)
verifySponsoredShortcutDoesNotExist(sponsoredShortcutTitle2, 3)
}
}
}

@ -234,7 +234,6 @@ class TabbedBrowsingTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun verifyPrivateTabUndoSnackBarTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -314,7 +313,6 @@ class TabbedBrowsingTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun verifyOpenTabDetails() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -328,7 +326,8 @@ class TabbedBrowsingTest {
verifyTabsTrayCounter()
verifyExistingTabList()
verifyNormalBrowsingNewTabButton()
verifyOpenedTabThumbnail()
// Disabled step due to ongoing tabs tray compose refactoring, see: https://github.com/mozilla-mobile/fenix/issues/21318
// verifyOpenedTabThumbnail()
verifyExistingOpenTabs(defaultWebPage.title)
verifyCloseTabsButton(defaultWebPage.title)
}.openTab(defaultWebPage.title) {

@ -11,6 +11,7 @@ import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.ui.robots.homeScreen
/**
@ -44,50 +45,155 @@ class ThreeDotMenuMainTest {
fun homeThreeDotMenuItemsTest() {
homeScreen {
}.openThreeDotMenu {
verifyBookmarksButton()
verifyHistoryButton()
verifyDownloadsButton()
verifyAddOnsButton()
// Disabled step due to https://github.com/mozilla-mobile/fenix/issues/26788
// verifySyncSignInButton()
verifyDesktopSite()
verifyWhatsNewButton()
verifyHelpButton()
verifyCustomizeHomeButton()
verifySettingsButton()
}.openSettings {
verifySettingsView()
verifyHomeThreeDotMainMenuItems(isRequestDesktopSiteEnabled = false)
}.openBookmarks {
verifyBookmarksMenuView()
}.goBack {
}.openThreeDotMenu {
}.openCustomizeHome {
verifyHomePageView()
}.openHistory {
verifyHistoryMenuView()
}.goBack {
}.openThreeDotMenu {
}.openDownloadsManager {
verifyEmptyDownloadsList()
}.goBack {
}.openThreeDotMenu {
}.openAddonsManagerMenu {
verifyAddonsItems()
}.goBack {
}.openThreeDotMenu {
}.openSyncSignIn {
verifyTurnOnSyncMenu()
}.goBack {
// Desktop toggle
}.openThreeDotMenu {
}.switchDesktopSiteMode {
}
homeScreen {
}.openThreeDotMenu {
verifyDesktopSiteModeEnabled(isRequestDesktopSiteEnabled = true)
}.openWhatsNew {
verifyWhatsNewURL()
}.goToHomescreen {
}.openThreeDotMenu {
}.openHelp {
verifyHelpUrl()
}.openTabDrawer {
closeTab()
}.goToHomescreen {
}.openThreeDotMenu {
}.openCustomizeHome {
verifyHomePageView()
}.goBack {
}.openThreeDotMenu {
}.openSettings {
verifySettingsView()
}
}
// Verifies the list of items in the homescreen's 3 dot main menu in private browsing
@Test
fun privateHomeThreeDotMenuItemsTest() {
homeScreen {
}.togglePrivateBrowsingMode()
homeScreen {
}.openThreeDotMenu {
verifyHomeThreeDotMainMenuItems(isRequestDesktopSiteEnabled = false)
}.openBookmarks {
verifyBookmarksMenuView()
}.goBack {
}.openThreeDotMenu {
}.openHistory {
verifyHistoryMenuView()
}.goBack {
}.openThreeDotMenu {
}.openDownloadsManager {
verifyEmptyDownloadsList()
}.goBack {
}.openThreeDotMenu {
}.openAddonsManagerMenu {
verifyAddonsItems()
}.goBack {
}.openThreeDotMenu {
}.openSyncSignIn {
verifyTurnOnSyncMenu()
}.goBack {
// Desktop toggle
}.openThreeDotMenu {
}.switchDesktopSiteMode {
}
homeScreen {
}.openThreeDotMenu {
verifyDesktopSiteModeEnabled(isRequestDesktopSiteEnabled = true)
}.openWhatsNew {
verifyWhatsNewURL()
}.openTabDrawer {
closeTab()
}.goToHomescreen {
}.openThreeDotMenu {
}.openHelp {
verifyHelpUrl()
}.goToHomescreen {
}.openThreeDotMenu {
}.openCustomizeHome {
verifyHomePageView()
}.goBack {
}.openThreeDotMenu {
}.openSettings {
verifySettingsView()
}
}
@Test
fun setDesktopSiteBeforePageLoadTest() {
val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 4)
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
verifyBookmarksMenuView()
}.closeMenu {
verifyDesktopSiteModeEnabled(false)
}.switchDesktopSiteMode {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openThreeDotMenu {
verifyDesktopSiteModeEnabled(true)
}.closeBrowserMenuToBrowser {
clickLinkMatchingText("Link 1")
}.openThreeDotMenu {
verifyDesktopSiteModeEnabled(true)
}.closeBrowserMenuToBrowser {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
longClickLink("Link 2")
clickContextOpenLinkInNewTab()
snackBarButtonClick()
}.openThreeDotMenu {
verifyDesktopSiteModeEnabled(false)
}
}
@Test
fun privateBrowsingSetDesktopSiteBeforePageLoadTest() {
val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 4)
homeScreen {
}.togglePrivateBrowsingMode()
homeScreen {
}.openThreeDotMenu {
}.openHistory {
verifyHistoryMenuView()
verifyDesktopSiteModeEnabled(false)
}.switchDesktopSiteMode {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openThreeDotMenu {
verifyDesktopSiteModeEnabled(true)
}.closeBrowserMenuToBrowser {
clickLinkMatchingText("Link 1")
}.openThreeDotMenu {
verifyDesktopSiteModeEnabled(true)
}.closeBrowserMenuToBrowser {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
longClickLink("Link 2")
clickContextOpenLinkInPrivateTab()
snackBarButtonClick()
}.openThreeDotMenu {
verifyDesktopSiteModeEnabled(false)
}
}
}

@ -9,12 +9,12 @@ 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.R
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.Constants.defaultTopSitesList
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.RetryTestRule
import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
@ -67,7 +67,7 @@ class TopSitesTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
expandMenu()
verifyAddToTopSitesButton()
verifyAddToShortcutsButton()
}.addToFirefoxHome {
verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
}.goToHomescreen {
@ -84,7 +84,7 @@ class TopSitesTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
expandMenu()
verifyAddToTopSitesButton()
verifyAddToShortcutsButton()
}.addToFirefoxHome {
verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
}.goToHomescreen {
@ -111,7 +111,7 @@ class TopSitesTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
expandMenu()
verifyAddToTopSitesButton()
verifyAddToShortcutsButton()
}.addToFirefoxHome {
verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
}.goToHomescreen {
@ -134,7 +134,7 @@ class TopSitesTest {
waitForPageToLoad()
}.openThreeDotMenu {
expandMenu()
verifyAddToTopSitesButton()
verifyAddToShortcutsButton()
}.addToFirefoxHome {
verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
}.goToHomescreen {
@ -156,7 +156,7 @@ class TopSitesTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
expandMenu()
verifyAddToTopSitesButton()
verifyAddToShortcutsButton()
}.addToFirefoxHome {
verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
}.goToHomescreen {
@ -169,6 +169,28 @@ class TopSitesTest {
}
}
@Test
fun verifyUndoRemoveTopSite() {
val defaultWebPage = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
expandMenu()
verifyAddToShortcutsButton()
}.addToFirefoxHome {
verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
}.goToHomescreen {
verifyExistingTopSitesList()
verifyExistingTopSitesTabs(defaultWebPage.title)
}.openContextMenuOnTopSitesWithTitle(defaultWebPage.title) {
verifyTopSiteContextMenuItems()
}.removeTopSite {
clickUndoSnackBarButton()
verifyExistingTopSitesTabs(defaultWebPage.title)
}
}
@Test
fun verifyRemoveTopSiteFromMainMenu() {
val defaultWebPage = getGenericAsset(mockWebServer, 1)
@ -177,7 +199,7 @@ class TopSitesTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
expandMenu()
verifyAddToTopSitesButton()
verifyAddToShortcutsButton()
}.addToFirefoxHome {
verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
}.goToHomescreen {
@ -192,21 +214,15 @@ class TopSitesTest {
}
}
// Expected for en-us defaults
@Test
fun verifyDefaultTopSitesLocale_EN() {
// en-US defaults
val defaultTopSites = arrayOf(
"Top Articles",
"Wikipedia",
"Google",
)
fun verifyDefaultTopSitesList() {
homeScreen { }.dismissOnboarding()
homeScreen {
verifyExistingTopSitesList()
defaultTopSites.forEach { item ->
verifyExistingTopSitesTabs(item)
defaultTopSitesList.values.forEach { value ->
verifyExistingTopSitesTabs(value)
}
}
}
@ -236,71 +252,4 @@ class TopSitesTest {
verifyEmptyHistoryView()
}
}
@SmokeTest
@Test
fun verifySponsoredShortcutsListTest() {
homeScreen {
var sponsoredShortcutTitle = getSponsoredShortcutTitle(2)
var sponsoredShortcutTitle2 = getSponsoredShortcutTitle(3)
verifyExistingSponsoredTopSitesTabs(sponsoredShortcutTitle, 2)
verifyExistingSponsoredTopSitesTabs(sponsoredShortcutTitle2, 3)
}.openThreeDotMenu {
}.openCustomizeHome {
verifySponsoredShortcutsCheckBox(true)
clickSponsoredShortcuts()
verifySponsoredShortcutsCheckBox(false)
}.goBack {
verifyNotExistingSponsoredTopSitesList()
}
}
@Test
fun openSponsoredShortcutTest() {
var sponsoredShortcutTitle = ""
homeScreen {
sponsoredShortcutTitle = getSponsoredShortcutTitle(2)
}.openSponsoredShortcut(sponsoredShortcutTitle) {
verifyUrl(sponsoredShortcutTitle)
}
}
@Test
fun openSponsoredShortcutInPrivateBrowsingTest() {
var sponsoredShortcutTitle = ""
homeScreen {
sponsoredShortcutTitle = getSponsoredShortcutTitle(2)
}.openContextMenuOnSponsoredShortcut(sponsoredShortcutTitle) {
}.openTopSiteInPrivateTab {
verifyUrl(sponsoredShortcutTitle)
}
}
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/25926")
@Test
fun verifySponsoredShortcutsSponsorsAndPrivacyOptionTest() {
var sponsoredShortcutTitle = ""
homeScreen {
sponsoredShortcutTitle = getSponsoredShortcutTitle(2)
}.openContextMenuOnSponsoredShortcut(sponsoredShortcutTitle) {
}.clickSponsorsAndPrivacyButton {
verifyUrl("support.mozilla.org/en-US/kb/sponsor-privacy")
}
}
@Test
fun verifySponsoredShortcutsSettingsOptionTest() {
var sponsoredShortcutTitle = ""
homeScreen {
sponsoredShortcutTitle = getSponsoredShortcutTitle(2)
}.openContextMenuOnSponsoredShortcut(sponsoredShortcutTitle) {
}.clickSponsoredShortcutsSettingsButton {
verifyHomePageView()
}
}
}

@ -6,24 +6,22 @@ package org.mozilla.fenix.ui.robots
import android.os.Build
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.clearText
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers.allOf
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull
import java.util.regex.Pattern
/**
@ -37,18 +35,15 @@ class AddToHomeScreenRobot {
fun clickAddPrivateBrowsingShortcutButton() = addPrivateBrowsingShortcutButton().click()
fun addShortcutName(title: String) {
mDevice.waitNotNull(Until.findObject(By.text("Add to Home screen")), waitingTime)
shortcutNameField()
.perform(clearText())
.perform(typeText(title))
}
fun addShortcutName(title: String) = shortcutTextField.setText(title)
fun verifyShortcutNameField(expectedText: String) = assertShortcutNameField(expectedText)
fun verifyShortcutTextFieldTitle(title: String) = assertItemContainingTextExists(shortcutTitle(title))
fun clickAddShortcutButton() = addButton().click()
fun clickAddShortcutButton() =
confirmAddToHomeScreenButton.clickAndWaitForNewWindow(waitingTime)
fun clickCancelShortcutButton() = cancelAddToHomeScreenButton().click()
fun clickCancelShortcutButton() =
cancelAddToHomeScreenButton.click()
fun clickAddAutomaticallyButton() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -106,22 +101,6 @@ fun addToHomeScreen(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenR
return AddToHomeScreenRobot.Transition()
}
private fun shortcutNameField() = onView(withId(R.id.shortcut_text))
private fun assertShortcutNameField(expectedText: String) {
onView(
allOf(
withId(R.id.shortcut_text),
withText(expectedText),
),
)
.check(matches(isCompletelyDisplayed()))
}
private fun addButton() = onView((withText("ADD")))
private fun cancelAddToHomeScreenButton() = onView((withText("CANCEL")))
private fun addAutomaticallyButton() =
mDevice.findObject(UiSelector().textContains("add automatically"))
@ -134,3 +113,12 @@ private fun noThanksPrivateBrowsingShortcutButton() = onView(withId(R.id.cfr_neg
private fun assertNoThanksPrivateBrowsingShortcutButton() = noThanksPrivateBrowsingShortcutButton()
.check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private val cancelAddToHomeScreenButton =
itemWithResId("$packageName:id/cancel_button")
private val confirmAddToHomeScreenButton =
itemWithResId("$packageName:id/add_button")
private val shortcutTextField =
itemWithResId("$packageName:id/shortcut_text")
private fun shortcutTitle(title: String) =
itemWithResIdAndText("$packageName:id/shortcut_text", title)

@ -258,6 +258,13 @@ class BookmarksRobot {
SettingsTurnOnSyncRobot().interact()
return SettingsTurnOnSyncRobot.Transition()
}
fun goBack(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
goBackButton().click()
HomeScreenRobot().interact()
return HomeScreenRobot.Transition()
}
}
}

@ -44,6 +44,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.MatcherHelper
import org.mozilla.fenix.helpers.SessionLoadedIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeLong
@ -911,22 +912,32 @@ class BrowserRobot {
}
fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
mDevice.waitForObjects(
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/mozac_browser_toolbar_browser_actions"),
),
waitingTime,
)
tabsCounter().click()
mDevice.waitForObjects(
mDevice.findObject(
UiSelector().resourceId("$packageName:id/new_tab_button"),
),
waitingTime,
)
for (i in 1..RETRY_COUNT) {
try {
mDevice.waitForObjects(
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/mozac_browser_toolbar_browser_actions"),
),
waitingTime,
)
tabsCounter().click()
assertTrue(
MatcherHelper.itemWithResId("$packageName:id/new_tab_button")
.waitForExists(waitingTime),
)
break
} catch (e: AssertionError) {
if (i == RETRY_COUNT) {
throw e
} else {
mDevice.waitForIdle()
}
}
}
assertTrue(MatcherHelper.itemWithResId("$packageName:id/new_tab_button").waitForExists(waitingTime))
TabDrawerRobot().interact()
return TabDrawerRobot.Transition()

@ -7,6 +7,7 @@ package org.mozilla.fenix.ui.robots
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput
@ -15,6 +16,7 @@ import androidx.compose.ui.test.swipeRight
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.action.ViewActions.pressImeActionButton
import androidx.test.espresso.matcher.RootMatchers
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiScrollable
@ -23,10 +25,12 @@ import androidx.test.uiautomator.Until
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull
class CollectionRobot {
@ -57,7 +61,8 @@ class CollectionRobot {
// names a collection saved from tab drawer
fun typeCollectionNameAndSave(collectionName: String) {
collectionNameTextField().text = collectionName
mDevice.findObject(UiSelector().textContains("OK")).click()
addCollectionButtonPanel.waitForExists(waitingTime)
addCollectionOkButton.click()
}
fun verifyTabsSelectedCounterText(numOfTabs: Int) {
@ -289,3 +294,7 @@ private fun backButton() =
mDevice.findObject(
UiSelector().resourceId("$packageName:id/back_button"),
)
private val addCollectionButtonPanel =
itemWithResId("$packageName:id/buttonPanel")
private val addCollectionOkButton = onView(withId(android.R.id.button1)).inRoot(RootMatchers.isDialog())

@ -123,6 +123,13 @@ class DownloadRobot {
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun goBack(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
goBackButton().click()
HomeScreenRobot().interact()
return HomeScreenRobot.Transition()
}
}
}
@ -195,3 +202,5 @@ private fun assertDownloadedFileIcon() =
mDevice.findObject(UiSelector().resourceId("$packageName:id/favicon"))
.exists(),
)
private fun goBackButton() = onView(withContentDescription("Navigate up"))

@ -37,7 +37,6 @@ import androidx.test.espresso.matcher.ViewMatchers.withHint
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiObject
import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
@ -54,6 +53,16 @@ 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.MatcherHelper.assertCheckedItemWithResIdExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdAndDescriptionExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdAndTextExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdExists
import org.mozilla.fenix.helpers.MatcherHelper.checkedItemWithResId
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.itemWithResIdAndText
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
import org.mozilla.fenix.helpers.TestHelper.appContext
@ -76,15 +85,15 @@ class HomeScreenRobot {
" service provider, it makes it easier to keep what you do online private from anyone" +
" else who uses this device."
fun verifyNavigationToolbar() = assertAppItemsWithResourceId(navigationToolbar)
fun verifyNavigationToolbar() = assertItemWithResIdExists(navigationToolbar)
fun verifyFocusedNavigationToolbar() = assertFocusedNavigationToolbar()
fun verifyHomeScreen() = assertAppItemsWithResourceId(homeScreen)
fun verifyHomeScreen() = assertItemWithResIdExists(homeScreen)
fun verifyHomeScreenAppBarItems() =
assertAppItemsWithResourceId(homeScreen, privateBrowsingButton, homepageWordmark)
assertItemWithResIdExists(homeScreen, privateBrowsingButton, homepageWordmark)
fun verifyHomeScreenWelcomeItems() =
assertAppItemsContainingText(welcomeHeader, welcomeSubHeader)
assertItemContainingTextExists(welcomeHeader, welcomeSubHeader)
fun verifyChooseYourThemeCard(
isDarkThemeChecked: Boolean,
@ -92,28 +101,37 @@ class HomeScreenRobot {
isAutomaticThemeChecked: Boolean,
) {
scrollToElementByText(getStringResource(R.string.onboarding_theme_picker_header))
assertAppItemsContainingText(
assertItemContainingTextExists(
chooseThemeHeader,
chooseThemeText,
darkThemeDescription,
lightThemeDescription,
)
assertAppItemsStateWithResourceId(
assertCheckedItemWithResIdExists(
darkThemeToggle(isDarkThemeChecked),
lightThemeToggle(isLightThemeChecked),
automaticThemeToggle(isAutomaticThemeChecked),
)
assertAppItemsWithResourceIdAndDescription(automaticThemeDescription)
assertItemWithResIdAndDescriptionExists(automaticThemeDescription)
}
fun clickLightThemeButton() =
itemWithResId("$packageName:id/theme_light_radio_button").click()
fun clickDarkThemeButton() =
itemWithResId("$packageName:id/theme_dark_radio_button").click()
fun clickAutomaticThemeButton() =
itemWithResId("$packageName:id/theme_automatic_radio_button").click()
fun verifyToolbarPlacementCard(isBottomChecked: Boolean, isTopChecked: Boolean) {
scrollToElementByText(getStringResource(R.string.onboarding_toolbar_placement_header_1))
assertAppItemsContainingText(toolbarPlacementHeader, toolbarPlacementDescription)
assertAppItemsStateWithResourceId(
assertItemContainingTextExists(toolbarPlacementHeader, toolbarPlacementDescription)
assertCheckedItemWithResIdExists(
toolbarPlacementBottomRadioButton(isBottomChecked),
toolbarPlacementTopRadioButton(isTopChecked),
)
assertAppItemsWithResourceId(
assertItemWithResIdExists(
toolbarPlacementBottomImage,
toolbarPlacementBottomTitle,
toolbarPlacementTopImage,
@ -121,48 +139,60 @@ class HomeScreenRobot {
)
}
fun clickTopToolbarPlacementButton() =
itemWithResId("$packageName:id/toolbar_top_radio_button").click()
fun clickBottomToolbarPlacementButton() =
itemWithResId("$packageName:id/toolbar_bottom_radio_button").click()
fun verifySignInToSyncCard() {
scrollToElementByText(getStringResource(R.string.onboarding_account_sign_in_header))
assertAppItemsContainingText(startSyncHeader, startSyncDescription)
assertAppItemsWithResourceId(signInButton)
assertItemContainingTextExists(startSyncHeader, startSyncDescription)
assertItemWithResIdExists(signInButton)
}
fun verifyPrivacyProtectionCard(isStandardChecked: Boolean, isStrictChecked: Boolean) {
scrollToElementByText(getStringResource(R.string.onboarding_tracking_protection_header))
assertAppItemsContainingText(privacyProtectionHeader, privacyProtectionDescription)
assertAppItemsStateWithResourceId(
scrollToElementByText(getStringResource(R.string.onboarding_privacy_notice_header_1))
assertItemContainingTextExists(privacyProtectionHeader, privacyProtectionDescription)
assertCheckedItemWithResIdExists(
standardTrackingProtectionToggle(isStandardChecked),
strictTrackingProtectionToggle(isStrictChecked),
)
}
fun clickStandardTrackingProtectionButton() =
itemWithResId("$packageName:id/tracking_protection_standard_option").click()
fun clickStrictTrackingProtectionButton() =
itemWithResId("$packageName:id/tracking_protection_strict_default").click()
fun verifyPrivacyNoticeCard() {
scrollToElementByText(getStringResource(R.string.onboarding_privacy_notice_header_1))
assertAppItemsContainingText(privacyNoticeHeader, privacyNoticeDescription)
assertAppItemsWithResourceId(privacyNoticeButton)
assertItemContainingTextExists(privacyNoticeHeader, privacyNoticeDescription)
assertItemWithResIdExists(privacyNoticeButton)
}
fun verifyStartBrowsingSection() {
scrollToElementByText(getStringResource(R.string.onboarding_finish))
assertAppItemsWithResourceId(startBrowsingButton)
assertAppItemsContainingText(conclusionHeader)
assertItemWithResIdExists(startBrowsingButton)
assertItemContainingTextExists(conclusionHeader)
}
fun verifyNavigationToolbarItems(numberOfOpenTabs: String) {
assertAppItemsWithResourceId(navigationToolbar, menuButton)
assertAppItemsWithResourceIdAndText(tabCounter(numberOfOpenTabs))
assertItemWithResIdExists(navigationToolbar, menuButton)
assertItemWithResIdAndTextExists(tabCounter(numberOfOpenTabs))
}
fun verifyHomePrivateBrowsingButton() = assertAppItemsWithResourceId(privateBrowsingButton)
fun verifyHomeMenuButton() = assertAppItemsWithResourceId(menuButton)
fun verifyHomePrivateBrowsingButton() = assertItemWithResIdExists(privateBrowsingButton)
fun verifyHomeMenuButton() = assertItemWithResIdExists(menuButton)
fun verifyTabButton() = assertTabButton()
fun verifyCollectionsHeader() = assertCollectionsHeader()
fun verifyNoCollectionsText() = assertNoCollectionsText()
fun verifyHomeWordmark() = assertAppItemsWithResourceId(homepageWordmark)
fun verifyHomeWordmark() = assertItemWithResIdExists(homepageWordmark)
fun verifyHomeComponent() = assertHomeComponent()
fun verifyDefaultSearchEngine(searchEngine: String) = verifySearchEngineIcon(searchEngine)
fun verifyTabCounter(numberOfOpenTabs: String) =
assertAppItemsWithResourceIdAndText(tabCounter(numberOfOpenTabs))
assertItemWithResIdAndTextExists(tabCounter(numberOfOpenTabs))
fun verifyKeyboardVisible() = assertKeyboardVisibility(isExpectedToBeVisible = true)
fun verifyWallpaperImageApplied(isEnabled: Boolean) {
@ -184,11 +214,14 @@ class HomeScreenRobot {
}
// First Run elements
fun verifyWelcomeHeader() = assertAppItemsContainingText(welcomeHeader)
fun verifyAccountsSignInButton() = assertAppItemsWithResourceId(signInButton)
fun verifyWelcomeHeader() = assertItemContainingTextExists(welcomeHeader)
fun verifyAccountsSignInButton() {
scrollToElementByText(getStringResource(R.string.onboarding_account_sign_in_header))
assertItemWithResIdExists(signInButton)
}
fun verifyStartBrowsingButton() {
scrollToElementByText(getStringResource(R.string.onboarding_finish))
assertAppItemsWithResourceId(startBrowsingButton)
assertItemWithResIdExists(startBrowsingButton)
}
// Upgrading users onboarding dialog
@ -233,9 +266,24 @@ class HomeScreenRobot {
fun verifyExistingTopSitesList() = assertExistingTopSitesList()
fun verifyNotExistingTopSitesList(title: String) = assertNotExistingTopSitesList(title)
fun verifySponsoredShortcutDoesNotExist(sponsoredShortcutTitle: String, position: Int) =
assertFalse(
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/top_site_item")
.index(position - 1),
).getChild(
UiSelector()
.textContains(sponsoredShortcutTitle),
).waitForExists(waitingTime),
)
fun verifyNotExistingSponsoredTopSitesList() = assertSponsoredTopSitesNotDisplayed()
fun verifyExistingTopSitesTabs(title: String) = assertExistingTopSitesTabs(title)
fun verifyExistingSponsoredTopSitesTabs(sponsoredShortcutTitle: String, position: Int) = assertSponsoredTopSiteIsDisplayed(sponsoredShortcutTitle, position)
fun verifySponsoredShortcutDetails(sponsoredShortcutTitle: String, position: Int) {
assertSponsoredShortcutLogoIsDisplayed(position)
assertSponsoredShortcutTitle(sponsoredShortcutTitle, position)
assertSponsoredSubtitleIsDisplayed(position)
}
fun verifyTopSiteContextMenuItems() = assertTopSiteContextMenuItems()
fun verifyJumpBackInSectionIsDisplayed() = assertJumpBackInSectionIsDisplayed()
@ -293,14 +341,7 @@ class HomeScreenRobot {
mDevice.waitNotNull(findObject(By.text(expectedText)), waitingTime)
}
fun clickUndoCollectionDeletion(expectedText: String) {
onView(
allOf(
withId(R.id.snackbar_btn),
withText(expectedText),
),
).click()
}
fun clickUndoSnackBarButton() = undoSnackBarButton.click()
fun clickFirefoxLogo() = homepageWordmark.click()
@ -429,19 +470,6 @@ class HomeScreenRobot {
}
}
fun getSponsoredShortcutTitle(position: Int): String {
val sponsoredShortcut = mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/top_site_item")
.index(position - 1),
).getChild(
UiSelector()
.resourceId("$packageName:id/top_site_title"),
).text
return sponsoredShortcut
}
fun verifyJumpBackInMessage() {
assertTrue(
mDevice.findObject(
@ -683,6 +711,7 @@ class HomeScreenRobot {
}
fun clickSaveTabsToCollectionButton(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
scrollToElementByText(getStringResource(R.string.no_collections_description2))
saveTabsToCollectionButton().click()
TabDrawerRobot().interact()
@ -780,6 +809,20 @@ class HomeScreenRobot {
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun clickSignInButton(interact: SyncSignInRobot.() -> Unit): SyncSignInRobot.Transition {
signInButton.clickAndWaitForNewWindow(waitingTimeShort)
SyncSignInRobot().interact()
return SyncSignInRobot.Transition()
}
fun clickPrivacyNoticeButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
privacyNoticeButton.clickAndWaitForNewWindow(waitingTimeShort)
BrowserRobot().interact()
return BrowserRobot.Transition()
}
}
}
@ -874,10 +917,17 @@ private fun assertExistingTopSitesTabs(title: String) {
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertSponsoredTopSiteIsDisplayed(sponsoredShortcutTitle: String, position: Int) {
assertSponsoredShortcutTitle(sponsoredShortcutTitle, position)
assertSponsoredSubtitleIsDisplayed(position)
}
private fun assertSponsoredShortcutLogoIsDisplayed(position: Int) =
assertTrue(
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/top_site_item")
.index(position - 1),
).getChild(
UiSelector()
.resourceId("$packageName:id/favicon_card"),
).waitForExists(waitingTime),
)
private fun assertSponsoredSubtitleIsDisplayed(position: Int) =
assertTrue(
@ -891,7 +941,7 @@ private fun assertSponsoredSubtitleIsDisplayed(position: Int) =
).waitForExists(waitingTime),
)
private fun assertSponsoredShortcutTitle(sponsoredShortcutTitle: String, position: Int) {
private fun assertSponsoredShortcutTitle(sponsoredShortcutTitle: String, position: Int) =
assertTrue(
mDevice.findObject(
UiSelector()
@ -902,7 +952,6 @@ private fun assertSponsoredShortcutTitle(sponsoredShortcutTitle: String, positio
.textContains(sponsoredShortcutTitle),
).waitForExists(waitingTime),
)
}
private fun assertNotExistingTopSitesList(title: String) {
mDevice.findObject(UiSelector().text(title)).waitUntilGone(waitingTime)
@ -1010,125 +1059,82 @@ private fun sponsoredShortcut(sponsoredShortcutTitle: String) =
private fun storyByTopicItem(composeTestRule: ComposeTestRule, position: Int) =
composeTestRule.onNodeWithTag("pocket.categories").onChildAt(position - 1)
private fun appItemWithResourceId(resourceId: String) =
mDevice.findObject(UiSelector().resourceId(resourceId))
private fun appItemContainingText(itemText: String) =
mDevice.findObject(UiSelector().textContains(itemText))
private fun appItemStateWithResourceId(resourceId: String, state: Boolean) =
mDevice.findObject(UiSelector().resourceId(resourceId).checked(state))
private fun appItemWithResourceIdAndDescription(resourceId: String, description: String) =
mDevice.findObject(UiSelector().resourceId(resourceId).descriptionContains(description))
private fun appItemWithResourceIdAndText(resourceId: String, text: String) =
mDevice.findObject(UiSelector().resourceId(resourceId).text(text))
private fun assertAppItemsWithResourceId(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
}
private fun assertAppItemsContainingText(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
}
private fun assertAppItemsStateWithResourceId(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
}
private fun assertAppItemsWithResourceIdAndDescription(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
}
private fun assertAppItemsWithResourceIdAndText(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
}
private val homeScreen =
appItemWithResourceId("$packageName:id/homeLayout")
itemWithResId("$packageName:id/homeLayout")
private val privateBrowsingButton =
appItemWithResourceId("$packageName:id/privateBrowsingButton")
itemWithResId("$packageName:id/privateBrowsingButton")
private val homepageWordmark =
appItemWithResourceId("$packageName:id/wordmark")
private val welcomeHeader = appItemContainingText(getStringResource(R.string.onboarding_header_2))
itemWithResId("$packageName:id/wordmark")
private val welcomeHeader = itemContainingText(getStringResource(R.string.onboarding_header_2))
private val welcomeSubHeader =
appItemContainingText(getStringResource(R.string.onboarding_message))
itemContainingText(getStringResource(R.string.onboarding_message))
private val chooseThemeHeader =
appItemContainingText(getStringResource(R.string.onboarding_theme_picker_header))
itemContainingText(getStringResource(R.string.onboarding_theme_picker_header))
private val chooseThemeText =
appItemContainingText(getStringResource(R.string.onboarding_theme_picker_description_2))
itemContainingText(getStringResource(R.string.onboarding_theme_picker_description_2))
private val darkThemeDescription =
appItemContainingText(getStringResource(R.string.onboarding_theme_dark_title))
itemContainingText(getStringResource(R.string.onboarding_theme_dark_title))
private val lightThemeDescription =
appItemContainingText(getStringResource(R.string.onboarding_theme_light_title))
itemContainingText(getStringResource(R.string.onboarding_theme_light_title))
private val automaticThemeDescription =
appItemWithResourceIdAndDescription(
itemWithResIdAndDescription(
"$packageName:id/clickable_region_automatic",
"${getStringResource(R.string.onboarding_theme_automatic_title)} ${getStringResource(R.string.onboarding_theme_automatic_summary)}",
)
private fun darkThemeToggle(isChecked: Boolean) =
appItemStateWithResourceId("$packageName:id/theme_dark_radio_button", isChecked)
checkedItemWithResId("$packageName:id/theme_dark_radio_button", isChecked)
private fun lightThemeToggle(isChecked: Boolean) =
appItemStateWithResourceId("$packageName:id/theme_light_radio_button", isChecked)
checkedItemWithResId("$packageName:id/theme_light_radio_button", isChecked)
private fun automaticThemeToggle(isChecked: Boolean) =
appItemStateWithResourceId("$packageName:id/theme_automatic_radio_button", isChecked)
checkedItemWithResId("$packageName:id/theme_automatic_radio_button", isChecked)
private val toolbarPlacementHeader =
appItemContainingText(getStringResource(R.string.onboarding_toolbar_placement_header_1))
itemContainingText(getStringResource(R.string.onboarding_toolbar_placement_header_1))
private val toolbarPlacementDescription =
appItemContainingText(getStringResource(R.string.onboarding_toolbar_placement_description))
itemContainingText(getStringResource(R.string.onboarding_toolbar_placement_description))
private fun toolbarPlacementBottomRadioButton(isChecked: Boolean) =
appItemStateWithResourceId("$packageName:id/toolbar_bottom_radio_button", isChecked)
checkedItemWithResId("$packageName:id/toolbar_bottom_radio_button", isChecked)
private fun toolbarPlacementTopRadioButton(isChecked: Boolean) =
appItemStateWithResourceId("$packageName:id/toolbar_top_radio_button", isChecked)
checkedItemWithResId("$packageName:id/toolbar_top_radio_button", isChecked)
private val toolbarPlacementBottomImage =
appItemWithResourceId("$packageName:id/toolbar_bottom_image")
itemWithResId("$packageName:id/toolbar_bottom_image")
private val toolbarPlacementBottomTitle =
appItemWithResourceId("$packageName:id/toolbar_bottom_title")
itemWithResId("$packageName:id/toolbar_bottom_title")
private val toolbarPlacementTopTitle =
appItemWithResourceId("$packageName:id/toolbar_top_title")
itemWithResId("$packageName:id/toolbar_top_title")
private val toolbarPlacementTopImage =
appItemWithResourceId("$packageName:id/toolbar_top_image")
itemWithResId("$packageName:id/toolbar_top_image")
private val startSyncHeader =
appItemContainingText(getStringResource(R.string.onboarding_account_sign_in_header))
itemContainingText(getStringResource(R.string.onboarding_account_sign_in_header))
private val startSyncDescription =
appItemContainingText(getStringResource(R.string.onboarding_manual_sign_in_description))
itemContainingText(getStringResource(R.string.onboarding_manual_sign_in_description))
private val signInButton =
appItemWithResourceId("$packageName:id/fxa_sign_in_button")
itemWithResId("$packageName:id/fxa_sign_in_button")
private val privacyProtectionHeader =
appItemContainingText(getStringResource(R.string.onboarding_tracking_protection_header))
itemContainingText(getStringResource(R.string.onboarding_tracking_protection_header))
private val privacyProtectionDescription =
appItemContainingText(getStringResource(R.string.onboarding_tracking_protection_description))
itemContainingText(getStringResource(R.string.onboarding_tracking_protection_description))
private fun standardTrackingProtectionToggle(isChecked: Boolean) =
appItemStateWithResourceId("$packageName:id/tracking_protection_standard_option", isChecked)
checkedItemWithResId("$packageName:id/tracking_protection_standard_option", isChecked)
private fun strictTrackingProtectionToggle(isChecked: Boolean) =
appItemStateWithResourceId("$packageName:id/tracking_protection_strict_default", isChecked)
checkedItemWithResId("$packageName:id/tracking_protection_strict_default", isChecked)
private val privacyNoticeHeader =
appItemContainingText(getStringResource(R.string.onboarding_privacy_notice_header_1))
itemContainingText(getStringResource(R.string.onboarding_privacy_notice_header_1))
private val privacyNoticeDescription =
appItemContainingText(getStringResource(R.string.onboarding_privacy_notice_description))
itemContainingText(getStringResource(R.string.onboarding_privacy_notice_description))
private val privacyNoticeButton =
appItemWithResourceId("$packageName:id/read_button")
itemWithResId("$packageName:id/read_button")
private val startBrowsingButton =
appItemWithResourceId("$packageName:id/finish_button")
itemWithResId("$packageName:id/finish_button")
private val conclusionHeader =
appItemContainingText(getStringResource(R.string.onboarding_conclusion_header))
itemContainingText(getStringResource(R.string.onboarding_conclusion_header))
private val navigationToolbar =
appItemWithResourceId("$packageName:id/toolbar")
itemWithResId("$packageName:id/toolbar")
private val menuButton =
appItemWithResourceId("$packageName:id/menuButton")
itemWithResId("$packageName:id/menuButton")
private fun tabCounter(numberOfOpenTabs: String) =
appItemWithResourceIdAndText("$packageName:id/counter_text", numberOfOpenTabs)
itemWithResIdAndText("$packageName:id/counter_text", numberOfOpenTabs)
private val undoSnackBarButton =
itemWithResId("$packageName:id/snackbar_btn")
val deleteFromHistory =
onView(

@ -46,6 +46,7 @@ import org.mozilla.fenix.helpers.TestHelper.getStringResource
import org.mozilla.fenix.helpers.TestHelper.isPackageInstalled
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.TestHelper.waitForObjects
import org.mozilla.fenix.helpers.click
/**
@ -110,31 +111,40 @@ class SearchRobot {
}
}
fun verifyFirefoxSuggestResults(rule: ComposeTestRule, searchSuggestion: String) {
fun verifyFirefoxSuggestResults(rule: ComposeTestRule, searchTerm: String, vararg searchSuggestions: String) {
rule.waitForIdle()
for (i in 1..RETRY_COUNT) {
try {
rule.onNodeWithTag("mozac.awesomebar.suggestions")
.performScrollToNode(hasText(searchSuggestion))
.assertExists()
for (searchSuggestion in searchSuggestions) {
mDevice.waitForObjects(mDevice.findObject(UiSelector().textContains(searchSuggestion)))
rule.onNodeWithTag("mozac.awesomebar.suggestions")
.performScrollToNode(hasText(searchSuggestion))
.assertExists()
}
break
} catch (e: AssertionError) {
if (i == RETRY_COUNT) {
throw e
} else {
expandSearchSuggestionsList()
mDevice.pressBack()
homeScreen {
}.openSearch {
typeSearch(searchTerm)
}
}
}
}
}
fun verifyNoSuggestionsAreDisplayed(rule: ComposeTestRule, searchSuggestion: String) {
fun verifyNoSuggestionsAreDisplayed(rule: ComposeTestRule, vararg searchSuggestions: String) {
rule.waitForIdle()
assertFalse(
mDevice.findObject(UiSelector().textContains(searchSuggestion))
.waitForExists(waitingTime),
)
for (searchSuggestion in searchSuggestions) {
assertFalse(
mDevice.findObject(UiSelector().textContains(searchSuggestion))
.waitForExists(waitingTime),
)
}
}
fun verifyAllowSuggestionsInPrivateModeDialog() {
@ -282,10 +292,6 @@ class SearchRobot {
assertTranslatedFocusedNavigationToolbar(toolbarHintString)
fun verifySearchEngineShortcuts(rule: ComposeTestRule, vararg searchEngines: String) {
mDevice.findObject(
UiSelector().resourceId("$packageName:id/awesome_bar"),
).swipeUp(1)
for (searchEngine in searchEngines) {
rule.waitForIdle()
rule.onNodeWithText(searchEngine).assertIsDisplayed()
@ -365,14 +371,21 @@ class SearchRobot {
rule: ComposeTestRule,
interact: SettingsSubMenuSearchRobot.() -> Unit,
): SettingsSubMenuSearchRobot.Transition {
rule.onNodeWithText("Search engine settings")
.assertIsDisplayed()
.assertHasClickAction()
.performClick()
rule.onNodeWithText("Search engine settings").performClick()
SettingsSubMenuSearchRobot().interact()
return SettingsSubMenuSearchRobot.Transition()
}
fun clickSearchSuggestion(searchSuggestion: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.findObject(UiSelector().textContains(searchSuggestion)).also {
it.waitForExists(waitingTime)
it.clickAndWaitForNewWindow(waitingTimeShort)
}
BrowserRobot().interact()
return BrowserRobot.Transition()
}
}
}

@ -4,6 +4,9 @@
package org.mozilla.fenix.ui.robots
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.RootMatchers
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.uiautomator.UiSelector
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
@ -12,6 +15,7 @@ import org.mozilla.fenix.helpers.TestHelper.getStringResource
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.click
class SettingsSubMenuAutofillRobot {
@ -22,14 +26,10 @@ class SettingsSubMenuAutofillRobot {
deleteAddressButton.waitForExists(waitingTime)
deleteAddressButton.click()
}
fun clickCancelDeleteAddressButton() {
cancelDeleteAddressButton.waitForExists(waitingTime)
cancelDeleteAddressButton.click()
}
fun clickConfirmDeleteAddressButton() {
confirmDeleteAddressButton.waitForExists(waitingTime)
confirmDeleteAddressButton.click()
}
fun clickCancelDeleteAddressButton() = cancelDeleteAddressButton.click()
fun clickConfirmDeleteAddressButton() = confirmDeleteAddressButton.click()
fun clickSubRegionOption(subRegion: String) {
subRegionOption(subRegion).waitForExists(waitingTime)
subRegionOption(subRegion).click()
@ -81,10 +81,7 @@ class SettingsSubMenuAutofillRobot {
deleteCreditCardButton.click()
}
fun clickConfirmDeleteCreditCardButton() {
confirmDeleteCreditCardButton.waitForExists(waitingTime)
confirmDeleteCreditCardButton.click()
}
fun clickConfirmDeleteCreditCardButton() = confirmDeleteCreditCardButton.click()
fun clickExpiryMonthOption(expiryMonth: String) {
expiryMonthOption(expiryMonth).waitForExists(waitingTime)
@ -142,8 +139,8 @@ private val phoneTextInput = mDevice.findObject(UiSelector().resourceId("$packag
private val emailTextInput = mDevice.findObject(UiSelector().resourceId("$packageName:id/email_input"))
private val saveButton = mDevice.findObject(UiSelector().resourceId("$packageName:id/save_button"))
private val deleteAddressButton = mDevice.findObject(UiSelector().resourceId("$packageName:id/delete_address_button"))
private val cancelDeleteAddressButton = mDevice.findObject(UiSelector().resourceId("android:id/button2"))
private val confirmDeleteAddressButton = mDevice.findObject(UiSelector().resourceId("android:id/button1"))
private val cancelDeleteAddressButton = onView(withId(android.R.id.button2)).inRoot(RootMatchers.isDialog())
private val confirmDeleteAddressButton = onView(withId(android.R.id.button1)).inRoot(RootMatchers.isDialog())
private val addCreditCardButton = mDevice.findObject(UiSelector().textContains(getStringResource(R.string.preferences_credit_cards_add_credit_card)))
private val manageSavedCardsButton = mDevice.findObject(UiSelector().textContains(getStringResource(R.string.preferences_credit_cards_manage_saved_cards)))
@ -153,8 +150,8 @@ private val expiryMonthDropDown = mDevice.findObject(UiSelector().resourceId("$p
private val expiryYearDropDown = mDevice.findObject(UiSelector().resourceId("$packageName:id/expiry_year_drop_down"))
private val savedCreditCardNumber = mDevice.findObject(UiSelector().resourceId("$packageName:id/credit_card_logo"))
private val deleteCreditCardButton = mDevice.findObject(UiSelector().resourceId("$packageName:id/delete_credit_card_button"))
private val confirmDeleteCreditCardButton = mDevice.findObject(UiSelector().resourceId("android:id/button1"))
private val securedCreditCardsLaterButton = mDevice.findObject(UiSelector().resourceId("android:id/button2"))
private val confirmDeleteCreditCardButton = onView(withId(android.R.id.button1)).inRoot(RootMatchers.isDialog())
private val securedCreditCardsLaterButton = onView(withId(android.R.id.button2)).inRoot(RootMatchers.isDialog())
private fun savedAddress(firstName: String) = mDevice.findObject(UiSelector().textContains(firstName))
private fun subRegionOption(subRegion: String) = mDevice.findObject(UiSelector().textContains(subRegion))

@ -20,8 +20,6 @@ import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.Matchers.endsWith
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestHelper.getStringResource
import org.mozilla.fenix.helpers.TestHelper.hasCousin
@ -35,11 +33,6 @@ class SettingsSubMenuCustomizeRobot {
fun verifyThemes() = assertThemes()
fun verifyLightThemeApplied(expected: Boolean) =
assertFalse("Light theme not selected", expected)
fun verifyDarkThemeApplied(expected: Boolean) = assertTrue("Dark theme not selected", expected)
fun selectDarkMode() = darkModeToggle().click()
fun selectLightMode() = lightModeToggle().click()

@ -6,6 +6,11 @@
package org.mozilla.fenix.ui.robots
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.RootMatchers
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.uiautomator.UiSelector
import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
import org.junit.Assert.assertTrue
@ -50,8 +55,8 @@ private fun assertSecureConnectionSubMenu(pageTitle: String = "", url: String =
private fun assertClearSiteDataPrompt(url: String) {
assertTrue(clearSiteDataPrompt(url).waitForExists(waitingTime))
assertTrue(cancelClearSiteDataButton.waitForExists(waitingTime))
assertTrue(deleteSiteDataButton.waitForExists(waitingTime))
cancelClearSiteDataButton.check(matches(isDisplayed()))
deleteSiteDataButton.check(matches(isDisplayed()))
}
private fun quickActionSheet() =
@ -145,5 +150,5 @@ private fun clearSiteDataPrompt(url: String) =
.textContains(url),
)
private val cancelClearSiteDataButton = mDevice.findObject(UiSelector().resourceId("android:id/button2"))
private val deleteSiteDataButton = mDevice.findObject(UiSelector().resourceId("android:id/button1"))
private val cancelClearSiteDataButton = onView(withId(android.R.id.button2)).inRoot(RootMatchers.isDialog())
private val deleteSiteDataButton = onView(withId(android.R.id.button1)).inRoot(RootMatchers.isDialog())

@ -14,9 +14,7 @@ import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.GeneralLocation
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.longClick
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.RootMatchers
@ -32,7 +30,6 @@ import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import androidx.test.uiautomator.Until.findObject
import com.google.android.material.bottomsheet.BottomSheetBehavior
import junit.framework.AssertionFailedError
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertTrue
import org.hamcrest.CoreMatchers.allOf
@ -115,7 +112,7 @@ class TabDrawerRobot {
var retries = 0 // number of retries before failing, will stop at 2
while (!tabItem(title).waitUntilGone(waitingTimeShort) && retries < 3
) {
tab(title).perform(ViewActions.swipeRight())
tab(title).swipeRight(3)
retries++
}
}
@ -124,7 +121,7 @@ class TabDrawerRobot {
var retries = 0 // number of retries before failing, will stop at 2
while (!tabItem(title).waitUntilGone(waitingTimeShort) && retries < 3
) {
tab(title).perform(ViewActions.swipeLeft())
tab(title).swipeLeft(3)
retries++
}
}
@ -149,75 +146,14 @@ class TabDrawerRobot {
snackBarButton.click()
}
fun verifyTabMediaControlButtonState(action: String) {
try {
mDevice.findObject(
UiSelector().resourceId("$packageName:id/tab_tray_empty_view"),
).waitUntilGone(waitingTime)
mDevice.findObject(
UiSelector().resourceId("$packageName:id/tab_tray_grid_item"),
).waitForExists(waitingTime)
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/play_pause_button")
.descriptionContains(action),
).waitForExists(waitingTime)
assertTrue(
mDevice.findObject(UiSelector().descriptionContains(action))
.waitForExists(waitingTime),
)
} catch (e: AssertionFailedError) {
// In some cases the tab media button isn't updated after performing an action on it
println("Failed to update the state of the tab media button")
// Let's dismiss the tabs tray and try again
mDevice.pressBack()
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/toolbar"),
).waitForExists(waitingTime)
browserScreen {
}.openTabDrawer {
// Click again the tab media button
tabMediaControlButton().click()
mDevice.findObject(
UiSelector().resourceId("$packageName:id/tab_tray_empty_view"),
).waitUntilGone(waitingTime)
mDevice.findObject(
UiSelector().resourceId("$packageName:id/tab_tray_grid_item"),
).waitForExists(waitingTime)
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/play_pause_button")
.descriptionContains(action),
).waitForExists(waitingTime)
assertTrue(
mDevice.findObject(UiSelector().descriptionContains(action))
.waitForExists(waitingTime),
)
}
}
}
fun verifyTabMediaControlButtonState(action: String) =
assertTrue(tabMediaControlButton(action).waitForExists(waitingTime))
fun clickTabMediaControlButton(action: String) {
mDevice.waitNotNull(
Until.findObjects(
By
.res("$packageName:id/play_pause_button")
.descContains(action),
),
waitingTime,
)
tabMediaControlButton().click()
tabMediaControlButton(action).also {
it.waitForExists(waitingTime)
it.click()
}
}
fun clickSelectTabsOption() {
@ -245,7 +181,10 @@ class TabDrawerRobot {
waitingTime,
)
tab(title).perform(longClick())
tab(title).also {
it.waitForExists(waitingTime)
it.longClick()
}
}
fun createCollection(
@ -449,8 +388,8 @@ fun tabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
return TabDrawerRobot.Transition()
}
private fun tabMediaControlButton() =
mDevice.findObject(UiSelector().resourceId("$packageName:id/play_pause_button"))
private fun tabMediaControlButton(action: String) =
mDevice.findObject(UiSelector().descriptionContains(action))
private fun closeTabButton() =
mDevice.findObject(UiSelector().descriptionContains("Close tab"))
@ -459,9 +398,9 @@ private fun assertCloseTabsButton(title: String) =
assertTrue(
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/mozac_browser_tabstray_close")
.descriptionContains("Close tab $title"),
).waitForExists(waitingTime),
.descriptionContains("Close tab"),
).getFromParent(UiSelector().textContains(title))
.waitForExists(waitingTime),
)
private fun normalBrowsingButton() = onView(
@ -622,12 +561,7 @@ private val tabsList =
// This Espresso tab selector is used for actions that UIAutomator doesn't handle very well: swipe and long-tap
private fun tab(title: String) =
onView(
allOf(
withId(R.id.mozac_browser_tabstray_title),
withText(title),
),
)
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) =

@ -16,21 +16,27 @@ import androidx.test.espresso.matcher.RootMatchers
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiObjectNotFoundException
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.not
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.assertCheckedItemWithResIdAndTextExists
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.checkedItemWithResIdAndText
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeLong
import org.mozilla.fenix.helpers.TestHelper.getStringResource
@ -45,20 +51,12 @@ import org.mozilla.fenix.helpers.ext.waitNotNull
@Suppress("ForbiddenComment")
class ThreeDotMenuMainRobot {
fun verifyShareAllTabsButton() = assertShareAllTabsButton()
fun verifySettingsButton() = assertSettingsButton()
fun verifyCustomizeHomeButton() = assertCustomizeHomeButton()
fun verifyAddOnsButton() = assertAddOnsButton()
fun verifyHistoryButton() = assertHistoryButton()
fun verifyBookmarksButton() = assertBookmarksButton()
fun verifySyncSignInButton() = assertSyncSignInButton()
fun verifyHelpButton() = assertHelpButton()
fun verifySettingsButton() = assertItemContainingTextExists(settingsButton())
fun verifyHistoryButton() = assertItemContainingTextExists(historyButton)
fun verifyThreeDotMenuExists() = threeDotMenuRecyclerViewExists()
fun verifyForwardButton() = assertForwardButton()
fun verifyAddBookmarkButton() = assertAddBookmarkButton()
fun verifyAddBookmarkButton() = assertItemWithResIdAndTextExists(addBookmarkButton)
fun verifyEditBookmarkButton() = assertEditBookmarkButton()
fun verifyRefreshButton() = assertRefreshButton()
fun verifyCloseAllTabsButton() = assertCloseAllTabsButton()
fun verifyShareButton() = assertShareButton()
fun verifyReaderViewAppearance(visible: Boolean) = assertReaderViewAppearanceButton(visible)
fun expandMenu() {
@ -66,49 +64,61 @@ class ThreeDotMenuMainRobot {
}
fun verifyShareTabButton() = assertShareTabButton()
fun verifySaveCollection() = assertSaveCollectionButton()
fun verifySelectTabs() = assertSelectTabsButton()
fun verifyFindInPageButton() = assertFindInPageButton()
fun verifyWhatsNewButton() = assertWhatsNewButton()
fun verifyAddToTopSitesButton() = assertAddToTopSitesButton()
fun verifyFindInPageButton() = assertItemContainingTextExists(findInPageButton)
fun verifyAddToShortcutsButton() = assertItemContainingTextExists(addToShortcutsButton)
fun verifyRemoveFromShortcutsButton() = assertRemoveFromShortcutsButton()
fun verifyAddToMobileHome() = assertAddToMobileHome()
fun verifyDesktopSite() = assertDesktopSite()
fun verifyDownloadsButton() = assertDownloadsButton()
fun verifyShareTabsOverlay() = assertShareTabsOverlay()
fun verifyNewTabButton() = assertNormalBrowsingNewTabButton()
fun verifyReportSiteIssueButton() = assertReportSiteIssueButton()
fun verifyDesktopSiteModeEnabled(state: Boolean) {
fun verifyDesktopSiteModeEnabled(isRequestDesktopSiteEnabled: Boolean) {
expandMenu()
if (state) {
desktopSiteButton().check(matches(isChecked()))
} else {
desktopSiteButton().check(matches(not(isChecked())))
}
assertCheckedItemWithResIdAndTextExists(desktopSiteToggle(isRequestDesktopSiteEnabled))
}
fun verifyPageThreeDotMainMenuItems(isRequestDesktopSiteEnabled: Boolean) {
expandMenu()
assertItemContainingTextExists(
normalBrowsingNewTabButton,
bookmarksButton,
historyButton,
downloadsButton,
addOnsButton,
syncAndSaveDataButton,
findInPageButton,
desktopSiteButton,
reportSiteIssueButton,
addToHomeScreenButton,
addToShortcutsButton,
saveToCollectionButton,
settingsButton(),
)
assertCheckedItemWithResIdAndTextExists(addBookmarkButton)
assertCheckedItemWithResIdAndTextExists(desktopSiteToggle(isRequestDesktopSiteEnabled))
assertItemWithDescriptionExists(
backButton,
forwardButton,
shareButton,
refreshButton,
)
}
fun verifyPageThreeDotMainMenuItems() {
verifyNewTabButton()
verifyBookmarksButton()
verifyAddBookmarkButton()
verifyHistoryButton()
verifyDownloadsButton()
verifyAddOnsButton()
verifySyncSignInButton()
threeDotMenuRecyclerView().perform(swipeUp())
verifyFindInPageButton()
verifyDesktopSite()
threeDotMenuRecyclerView().perform(swipeUp())
verifyReportSiteIssueButton()
verifyAddToTopSitesButton()
verifyAddToMobileHome()
verifySaveCollection()
verifySettingsButton()
verifyShareButton()
verifyForwardButton()
verifyRefreshButton()
fun verifyHomeThreeDotMainMenuItems(isRequestDesktopSiteEnabled: Boolean) {
assertItemContainingTextExists(
bookmarksButton,
historyButton,
downloadsButton,
addOnsButton,
// Disabled step due to https://github.com/mozilla-mobile/fenix/issues/26788
// syncAndSaveDataButton,
desktopSiteButton,
whatsNewButton,
helpButton,
customizeHomeButton,
settingsButton(),
)
assertCheckedItemWithResIdAndTextExists(desktopSiteToggle(isRequestDesktopSiteEnabled))
}
private fun assertShareTabsOverlay() {
@ -164,7 +174,7 @@ class ThreeDotMenuMainRobot {
fun openDownloadsManager(interact: DownloadRobot.() -> Unit): DownloadRobot.Transition {
threeDotMenuRecyclerView().perform(swipeDown())
downloadsButton().click()
downloadsButton.click()
DownloadRobot().interact()
return DownloadRobot.Transition()
@ -173,7 +183,7 @@ class ThreeDotMenuMainRobot {
fun openSyncSignIn(interact: SyncSignInRobot.() -> Unit): SyncSignInRobot.Transition {
threeDotMenuRecyclerView().perform(swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("Sync and save data")), waitingTime)
syncSignInButton().click()
syncAndSaveDataButton.click()
SyncSignInRobot().interact()
return SyncSignInRobot.Transition()
@ -183,7 +193,7 @@ class ThreeDotMenuMainRobot {
threeDotMenuRecyclerView().perform(swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime)
bookmarksButton().click()
bookmarksButton.click()
assertTrue(mDevice.findObject(UiSelector().resourceId("$packageName:id/bookmark_list")).waitForExists(waitingTime))
BookmarksRobot().interact()
@ -193,7 +203,7 @@ class ThreeDotMenuMainRobot {
fun openHistory(interact: HistoryRobot.() -> Unit): HistoryRobot.Transition {
threeDotMenuRecyclerView().perform(swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("History")), waitingTime)
historyButton().click()
historyButton.click()
HistoryRobot().interact()
return HistoryRobot.Transition()
@ -201,7 +211,7 @@ class ThreeDotMenuMainRobot {
fun bookmarkPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime)
addBookmarkButton().click()
addBookmarkButton.click()
BrowserRobot().interact()
return BrowserRobot.Transition()
@ -217,7 +227,7 @@ class ThreeDotMenuMainRobot {
fun openHelp(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.text("Help")), waitingTime)
helpButton().click()
helpButton.click()
BrowserRobot().interact()
return BrowserRobot.Transition()
@ -232,7 +242,7 @@ class ThreeDotMenuMainRobot {
waitingTime,
)
customizeHomeButton().click()
customizeHomeButton.click()
mDevice.findObject(
UiSelector().resourceId("$packageName:id/recycler_view"),
@ -243,35 +253,27 @@ class ThreeDotMenuMainRobot {
}
fun goForward(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
forwardButton().click()
forwardButton.click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun goBack(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
backButton().click()
fun goToPreviousPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
backButton.click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun clickShareButton(interact: ShareOverlayRobot.() -> Unit): ShareOverlayRobot.Transition {
shareButton().click()
shareButton.click()
mDevice.waitNotNull(Until.findObject(By.text("ALL ACTIONS")), waitingTime)
ShareOverlayRobot().interact()
return ShareOverlayRobot.Transition()
}
fun close(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
// Close three dot
mDevice.pressBack()
HomeScreenRobot().interact()
return HomeScreenRobot.Transition()
}
fun closeBrowserMenuToBrowser(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
// Close three dot
mDevice.pressBack()
@ -281,8 +283,7 @@ class ThreeDotMenuMainRobot {
}
fun refreshPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
assertRefreshButton()
refreshButton().click()
refreshButton.click()
BrowserRobot().interact()
return BrowserRobot.Transition()
@ -298,7 +299,7 @@ class ThreeDotMenuMainRobot {
fun openReportSiteIssue(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
threeDotMenuRecyclerView().perform(swipeUp())
threeDotMenuRecyclerView().perform(swipeUp())
reportSiteIssueButton().click()
reportSiteIssueButton.click()
BrowserRobot().interact()
return BrowserRobot.Transition()
@ -308,7 +309,7 @@ class ThreeDotMenuMainRobot {
threeDotMenuRecyclerView().perform(swipeUp())
threeDotMenuRecyclerView().perform(swipeUp())
mDevice.waitNotNull(Until.findObject(By.text("Find in page")), waitingTime)
findInPageButton().click()
findInPageButton.click()
FindInPageRobot().interact()
return FindInPageRobot.Transition()
@ -316,7 +317,7 @@ class ThreeDotMenuMainRobot {
fun openWhatsNew(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.text("Whats new")), waitingTime)
whatsNewButton().click()
whatsNewButton.click()
BrowserRobot().interact()
return BrowserRobot.Transition()
@ -332,7 +333,26 @@ class ThreeDotMenuMainRobot {
}
fun addToFirefoxHome(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
addToTopSitesButton().click()
for (i in 1..RETRY_COUNT) {
try {
addToShortcutsButton.also {
it.waitForExists(waitingTime)
it.click()
}
break
} catch (e: UiObjectNotFoundException) {
if (i == RETRY_COUNT) {
throw e
} else {
mDevice.pressBack()
navigationToolbar {
}.openThreeDotMenu {
expandMenu()
}
}
}
}
BrowserRobot().interact()
return BrowserRobot.Transition()
@ -346,8 +366,7 @@ class ThreeDotMenuMainRobot {
}
fun openAddToHomeScreen(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.text("Add to Home screen")), waitingTime)
addToHomeScreenButton().click()
addToHomeScreenButton.clickAndWaitForNewWindow(waitingTime)
AddToHomeScreenRobot().interact()
return AddToHomeScreenRobot.Transition()
@ -368,7 +387,7 @@ class ThreeDotMenuMainRobot {
threeDotMenuRecyclerView().perform(swipeUp())
mDevice.waitNotNull(Until.findObject(By.text("Save to collection")), waitingTime)
saveCollectionButton().click()
saveToCollectionButton.click()
CollectionRobot().interact()
return CollectionRobot.Transition()
}
@ -396,7 +415,7 @@ class ThreeDotMenuMainRobot {
fun switchDesktopSiteMode(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
threeDotMenuRecyclerView().perform(swipeUp())
threeDotMenuRecyclerView().perform(swipeUp())
desktopSiteButton().click()
desktopSiteButton.click()
BrowserRobot().interact()
return BrowserRobot.Transition()
@ -417,61 +436,10 @@ private fun threeDotMenuRecyclerViewExists() {
threeDotMenuRecyclerView().check(matches(isDisplayed()))
}
private fun settingsButton(localizedText: String = getStringResource(R.string.browser_menu_settings)) =
mDevice.findObject(UiSelector().text(localizedText))
private fun assertSettingsButton() = assertTrue(settingsButton().waitForExists(waitingTime))
private fun customizeHomeButton() =
onView(
allOf(
withId(R.id.text),
withText(R.string.browser_menu_customize_home_1),
),
)
private fun assertCustomizeHomeButton() =
customizeHomeButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun addOnsButton() = onView(allOf(withText("Add-ons")))
private fun assertAddOnsButton() {
onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeDown())
addOnsButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun historyButton() = onView(allOf(withText(R.string.library_history)))
private fun assertHistoryButton() = historyButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun bookmarksButton() = onView(allOf(withText(R.string.library_bookmarks)))
private fun assertBookmarksButton() = bookmarksButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun syncSignInButton() = onView(withText("Sync and save data"))
private fun assertSyncSignInButton() = syncSignInButton().check(matches(isDisplayed()))
private fun helpButton() = onView(allOf(withText(R.string.browser_menu_help)))
private fun assertHelpButton() = helpButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun forwardButton() = mDevice.findObject(UiSelector().description("Forward"))
private fun assertForwardButton() = assertTrue(forwardButton().waitForExists(waitingTime))
private fun backButton() = mDevice.findObject(UiSelector().description("Back"))
private fun addBookmarkButton() = onView(allOf(withId(R.id.checkbox), withText("Add")))
private fun assertAddBookmarkButton() {
onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeUp())
addBookmarkButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun editBookmarkButton() = onView(withText("Edit"))
private fun assertEditBookmarkButton() = editBookmarkButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun refreshButton() = mDevice.findObject(UiSelector().description("Refresh"))
private fun assertRefreshButton() = assertTrue(refreshButton().waitForExists(waitingTime))
private fun stopLoadingButton() = onView(ViewMatchers.withContentDescription("Stop"))
private fun closeAllTabsButton() = onView(allOf(withText("Close all tabs"))).inRoot(RootMatchers.isPlatformPopup())
@ -482,36 +450,10 @@ private fun shareTabButton() = onView(allOf(withText("Share all tabs"))).inRoot(
private fun assertShareTabButton() = shareTabButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun shareButton() = mDevice.findObject(UiSelector().description("Share"))
private fun assertShareButton() = assertTrue(shareButton().waitForExists(waitingTime))
private fun saveCollectionButton() = onView(allOf(withText("Save to collection"))).inRoot(RootMatchers.isPlatformPopup())
private fun assertSaveCollectionButton() = saveCollectionButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun selectTabsButton() = onView(allOf(withText("Select tabs"))).inRoot(RootMatchers.isPlatformPopup())
private fun assertSelectTabsButton() = selectTabsButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun reportSiteIssueButton() = onView(withText("Report Site Issue…"))
private fun assertReportSiteIssueButton() = reportSiteIssueButton().check(matches(isDisplayed()))
private fun findInPageButton() = onView(allOf(withText("Find in page")))
private fun assertFindInPageButton() = findInPageButton()
private fun whatsNewButton() = onView(
allOf(
withText("Whats new"),
withEffectiveVisibility(Visibility.VISIBLE),
),
)
private fun assertWhatsNewButton() = whatsNewButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun addToHomeScreenButton() = onView(withText("Add to Home screen"))
private fun readerViewAppearanceToggle() =
mDevice.findObject(UiSelector().text("Customize reader view"))
@ -532,21 +474,9 @@ private fun assertReaderViewAppearanceButton(visible: Boolean) {
}
}
private fun addToTopSitesButton() =
onView(allOf(withText(R.string.browser_menu_add_to_shortcuts)))
private fun removeFromShortcutsButton() =
onView(allOf(withText(R.string.browser_menu_remove_from_shortcuts)))
private fun assertAddToTopSitesButton() {
onView(withId(R.id.mozac_browser_menu_recyclerView))
.perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText(R.string.browser_menu_add_to_shortcuts)),
),
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertRemoveFromShortcutsButton() {
onView(withId(R.id.mozac_browser_menu_recyclerView))
.perform(
@ -556,26 +486,8 @@ private fun assertRemoveFromShortcutsButton() {
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun addToMobileHomeButton() =
onView(allOf(withText(R.string.browser_menu_add_to_homescreen)))
private fun assertAddToMobileHome() {
onView(withId(R.id.mozac_browser_menu_recyclerView))
.perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText(R.string.browser_menu_add_to_homescreen)),
),
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun installPWAButton() = mDevice.findObject(UiSelector().text("Install"))
private fun desktopSiteButton() = onView(withId(R.id.switch_widget))
private fun assertDesktopSite() {
threeDotMenuRecyclerView().perform(swipeUp())
desktopSiteButton().check(matches(isDisplayed()))
}
private fun openInAppButton() =
onView(
allOf(
@ -584,15 +496,9 @@ private fun openInAppButton() =
),
)
private fun downloadsButton() = onView(withText(R.string.library_downloads))
private fun assertDownloadsButton() {
onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeDown())
downloadsButton().check(matches(isDisplayed()))
}
private fun clickAddonsManagerButton() {
onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeDown())
addOnsButton().check(matches(isCompletelyDisplayed())).click()
addOnsButton.click()
}
private fun shareAllTabsButton() =
@ -605,4 +511,45 @@ private fun assertShareAllTabsButton() {
)
}
private fun assertNormalBrowsingNewTabButton() = onView(withText("New tab")).check(matches(isDisplayed()))
private val bookmarksButton =
itemContainingText(getStringResource(R.string.library_bookmarks))
private val historyButton =
itemContainingText(getStringResource(R.string.library_history))
private val downloadsButton =
itemContainingText(getStringResource(R.string.library_downloads))
private val addOnsButton =
itemContainingText(getStringResource(R.string.browser_menu_add_ons))
private val desktopSiteButton =
itemContainingText(getStringResource(R.string.browser_menu_desktop_site))
private fun desktopSiteToggle(state: Boolean) =
checkedItemWithResIdAndText(
"$packageName:id/switch_widget",
getStringResource(R.string.browser_menu_desktop_site),
state,
)
private val whatsNewButton =
itemContainingText(getStringResource(R.string.browser_menu_whats_new))
private val helpButton =
itemContainingText(getStringResource(R.string.browser_menu_help))
private val customizeHomeButton =
itemContainingText(getStringResource(R.string.browser_menu_customize_home_1))
private fun settingsButton(localizedText: String = getStringResource(R.string.browser_menu_settings)) =
itemContainingText(localizedText)
private val syncAndSaveDataButton =
itemContainingText(getStringResource(R.string.sync_menu_sync_and_save_data))
private val normalBrowsingNewTabButton =
itemContainingText(getStringResource(R.string.library_new_tab))
private val addBookmarkButton =
itemWithResIdAndText(
"$packageName:id/checkbox",
getStringResource(R.string.browser_menu_add),
)
private val findInPageButton = itemContainingText(getStringResource(R.string.browser_menu_find_in_page))
private val reportSiteIssueButton = itemContainingText("Report Site Issue")
private val addToHomeScreenButton = itemContainingText(getStringResource(R.string.browser_menu_add_to_homescreen))
private val addToShortcutsButton = itemContainingText(getStringResource(R.string.browser_menu_add_to_shortcuts))
private val saveToCollectionButton = itemContainingText(getStringResource(R.string.browser_menu_save_to_collection_2))
private val backButton = itemWithDescription(getStringResource(R.string.browser_menu_back))
private val forwardButton = itemWithDescription(getStringResource(R.string.browser_menu_forward))
private val shareButton = itemWithDescription(getStringResource(R.string.share_button_content_description))
private val refreshButton = itemWithDescription(getStringResource(R.string.browser_menu_refresh))

@ -28,6 +28,7 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) {
FromAbout(R.id.aboutFragment),
FromTrackingProtection(R.id.trackingProtectionFragment),
FromHttpsOnlyMode(R.id.httpsOnlyFragment),
FromCookieBanner(R.id.cookieBannerFragment),
FromTrackingProtectionDialog(R.id.trackingProtectionPanelDialogFragment),
FromSavedLoginsFragment(R.id.savedLoginsFragment),
FromAddNewDeviceFragment(R.id.addNewDeviceFragment),

@ -67,10 +67,15 @@ object FeatureFlags {
*/
const val saveToPDF = true
/**
* Enables the notification pre permission prompt.
*/
const val notificationPrePermissionPromptEnabled = true
/**
* Enables storage maintenance feature.
*
* Feature flag tracking: https://github.com/mozilla-mobile/fenix/issues/27759
* */
val storageMaintenanceFeature = Config.channel.isNightlyOrDebug
val storageMaintenanceFeature = Config.channel.isNightlyOrDebug || Config.channel.isBeta
}

@ -61,6 +61,7 @@ import mozilla.components.support.webextensions.WebExtensionSupport
import org.mozilla.fenix.GleanMetrics.Addons
import org.mozilla.fenix.GleanMetrics.AndroidAutofill
import org.mozilla.fenix.GleanMetrics.CustomizeHome
import org.mozilla.fenix.GleanMetrics.Events.marketingNotificationAllowed
import org.mozilla.fenix.GleanMetrics.GleanBuildInfo
import org.mozilla.fenix.GleanMetrics.Metrics
import org.mozilla.fenix.GleanMetrics.PerfStartup
@ -73,14 +74,16 @@ 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.toolbar.ToolbarPosition
import org.mozilla.fenix.ext.areNotificationsEnabledSafe
import org.mozilla.fenix.ext.containsQueryParameters
import org.mozilla.fenix.ext.getCustomGleanServerUrlIfAvailable
import org.mozilla.fenix.ext.isCustomEngine
import org.mozilla.fenix.ext.isKnownSearchDomain
import org.mozilla.fenix.ext.isNotificationChannelEnabled
import org.mozilla.fenix.ext.setCustomEndpointIfAvailable
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.onboarding.ensureMarketingChannelExists
import org.mozilla.fenix.onboarding.MARKETING_CHANNEL_ID
import org.mozilla.fenix.perf.MarkersActivityLifecycleCallbacks
import org.mozilla.fenix.perf.ProfilerMarkerFactProcessor
import org.mozilla.fenix.perf.StartupTimeline
@ -203,6 +206,11 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
ProfilerMarkerFactProcessor.create { components.core.engine.profiler }.register()
run {
// Make sure the engine is initialized and ready to use.
components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
components.core.engine.warmUp()
}
// We need to always initialize Glean and do it early here.
initializeGlean()
@ -213,10 +221,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
components.strictMode.enableStrictMode(true)
warmBrowsersCache()
// Make sure the engine is initialized and ready to use.
components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
components.core.engine.warmUp()
}
initializeWebExtensionSupport()
if (FeatureFlags.storageMaintenanceFeature) {
// Make sure to call this function before registering a storage worker
@ -252,6 +256,8 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
ProcessLifecycleOwner.get().lifecycle.addObserver(TelemetryLifecycleObserver(components.core.store))
components.analytics.metricsStorage.tryRegisterAsUsageRecorder(this)
downloadWallpapers()
}
@ -368,16 +374,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
}
}
// For Android 13 or above, prompt the user for notification permission at the start.
// Regardless if the user accepts or denies the permission prompt, the prompt will occur only once.
fun queueNotificationPermissionRequest() {
if (SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
queue.runIfReadyOrQueue {
ensureMarketingChannelExists(this)
}
}
}
initQueue()
// We init these items in the visual completeness queue to avoid them initing in the critical
@ -387,7 +383,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
queueReviewPrompt()
queueRestoreLocale()
queueStorageMaintenance()
queueNotificationPermissionRequest()
}
private fun startMetricsIfEnabled() {
@ -749,14 +744,11 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
defaultWallpaper.set(isDefaultTheCurrentWallpaper)
@Suppress("TooGenericExceptionCaught")
try {
notificationsAllowed.set(
NotificationManagerCompat.from(applicationContext).areNotificationsEnabled(),
)
} catch (e: Exception) {
Logger.warn("Failed to check if notifications are enabled", e)
}
val notificationManagerCompat = NotificationManagerCompat.from(applicationContext)
notificationsAllowed.set(notificationManagerCompat.areNotificationsEnabledSafe())
marketingNotificationAllowed.set(
notificationManagerCompat.isNotificationChannelEnabled(MARKETING_CHANNEL_ID),
)
}
with(AndroidAutofill) {

@ -28,6 +28,7 @@ import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.Companion.PROTECTED
import androidx.appcompat.app.ActionBar
import androidx.appcompat.widget.Toolbar
import androidx.core.app.NotificationManagerCompat
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDestination
import androidx.navigation.NavDirections
@ -67,6 +68,7 @@ import mozilla.components.support.ktx.android.content.share
import mozilla.components.support.ktx.kotlin.isUrl
import mozilla.components.support.ktx.kotlin.toNormalizedUrl
import mozilla.components.support.locale.LocaleAwareAppCompatActivity
import mozilla.components.support.utils.ManufacturerCodes
import mozilla.components.support.utils.SafeIntent
import mozilla.components.support.utils.toSafeIntent
import mozilla.components.support.webextensions.WebExtensionPopupFeature
@ -84,12 +86,14 @@ import org.mozilla.fenix.components.metrics.BreadcrumbsRecorder
import org.mozilla.fenix.databinding.ActivityHomeBinding
import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExceptionsFragmentDirections
import org.mozilla.fenix.ext.alreadyOnDestination
import org.mozilla.fenix.ext.areNotificationsEnabledSafe
import org.mozilla.fenix.ext.breadcrumb
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.setNavigationIcon
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.home.intent.AssistIntentProcessor
import org.mozilla.fenix.home.intent.CrashReporterIntentProcessor
import org.mozilla.fenix.home.intent.DefaultBrowserIntentProcessor
import org.mozilla.fenix.home.intent.HomeDeepLinkIntentProcessor
@ -102,9 +106,11 @@ import org.mozilla.fenix.library.bookmarks.DesktopFolders
import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentDirections
import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.onboarding.DefaultBrowserNotificationWorker
import org.mozilla.fenix.onboarding.FenixOnboarding
import org.mozilla.fenix.onboarding.ReEngagementNotificationWorker
import org.mozilla.fenix.onboarding.ensureMarketingChannelExists
import org.mozilla.fenix.perf.MarkersActivityLifecycleCallbacks
import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks
import org.mozilla.fenix.perf.Performance
@ -115,6 +121,7 @@ import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.perf.StartupTypeTelemetry
import org.mozilla.fenix.search.SearchDialogFragmentDirections
import org.mozilla.fenix.session.PrivateNotificationService
import org.mozilla.fenix.settings.CookieBannersFragmentDirections
import org.mozilla.fenix.settings.HttpsOnlyFragmentDirections
import org.mozilla.fenix.settings.SettingsFragmentDirections
import org.mozilla.fenix.settings.TrackingProtectionFragmentDirections
@ -132,7 +139,6 @@ import org.mozilla.fenix.theme.DefaultThemeManager
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.trackingprotection.TrackingProtectionPanelDialogFragmentDirections
import org.mozilla.fenix.utils.BrowsersCache
import org.mozilla.fenix.utils.ManufacturerCodes
import org.mozilla.fenix.utils.Settings
import java.lang.ref.WeakReference
@ -182,6 +188,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
listOf(
HomeDeepLinkIntentProcessor(this),
SpeechProcessingIntentProcessor(this, components.core.store),
AssistIntentProcessor(),
StartSearchIntentProcessor(),
OpenBrowserIntentProcessor(this, ::getIntentSessionId),
OpenSpecificTabIntentProcessor(this),
@ -317,6 +324,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
}
}
showNotificationPermissionPromptIfRequired()
components.backgroundServices.accountManagerAvailableQueue.runIfReadyOrQueue {
lifecycleScope.launch(IO) {
// If we're authenticated, kick-off a sync and a device state refresh.
@ -334,6 +343,30 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
StartupTimeline.onActivityCreateEndHome(this) // DO NOT MOVE ANYTHING BELOW HERE.
}
/**
* On Android 13 or above, prompt the user for notification permission at the start.
* Show the pre permission dialog to the user once if the notification are not enabled.
*/
private fun showNotificationPermissionPromptIfRequired() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
!NotificationManagerCompat.from(applicationContext).areNotificationsEnabledSafe() &&
settings().numberOfAppLaunches <= 1
) {
// Recording the exposure event here to capture all users who met all criteria to receive
// the pre permission notification prompt
FxNimbus.features.prePermissionNotificationPrompt.recordExposure()
if (settings().notificationPrePermissionPromptEnabled) {
if (!settings().isNotificationPrePermissionShown) {
navHost.navController.navigate(NavGraphDirections.actionGlobalHomeNotificationPermissionDialog())
}
} else {
// This will trigger the notification permission system dialog as app targets sdk 32.
ensureMarketingChannelExists(applicationContext)
}
}
}
private fun checkAndExitPiP() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInPictureInPictureMode && intent != null) {
// Exit PiP mode
@ -848,6 +881,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
HistoryMetadataGroupFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTrackingProtectionExceptions ->
TrackingProtectionExceptionsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromCookieBanner ->
CookieBannersFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromHttpsOnlyMode ->
HttpsOnlyFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromAbout ->

@ -243,7 +243,12 @@ class InstalledAddonDetailsFragment : Fragment() {
val shouldCreatePrivateSession =
(activity as HomeActivity).browsingModeManager.mode.isPrivate
components.useCases.tabsUseCases.addTab(settingUrl, private = shouldCreatePrivateSession)
// If the addon settings page is already open in a tab, select that one
components.useCases.tabsUseCases.selectOrAddTab(
url = settingUrl,
private = shouldCreatePrivateSession,
ignoreFragment = true,
)
InstalledAddonDetailsFragmentDirections.actionGlobalBrowser(null)
} else {

@ -15,11 +15,14 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.thumbnails.BrowserThumbnails
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.concept.engine.permission.SitePermissions
@ -28,9 +31,11 @@ import mozilla.components.feature.contextmenu.ContextMenuCandidate
import mozilla.components.feature.readerview.ReaderViewFeature
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tabs.WindowFeature
import mozilla.components.lib.state.ext.consumeFlow
import mozilla.components.service.glean.private.NoExtras
import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import org.mozilla.fenix.GleanMetrics.ReaderMode
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
@ -43,6 +48,7 @@ import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.settings.quicksettings.protections.cookiebanners.dialog.CookieBannerReEngagementDialogUtils
import org.mozilla.fenix.shortcut.PwaOnboardingObserver
import org.mozilla.fenix.theme.ThemeManager
@ -173,6 +179,9 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
view = view,
)
}
if (!context.settings().shouldUseCookieBanner && !context.settings().userOptOutOfReEngageCookieBannerDialog) {
observeCookieBannerHandlingState(context.components.core.store)
}
}
override fun onUpdateToolbarForConfigurationChange(toolbar: BrowserToolbarView) {
@ -370,12 +379,17 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
useCase.containsException(tab.id) { hasTrackingProtectionException ->
lifecycleScope.launch(Dispatchers.Main) {
val cookieBannersStorage = requireComponents.core.cookieBannersStorage
val hasCookieBannerException = withContext(Dispatchers.IO) {
cookieBannersStorage.hasException(
tab.content.url,
tab.content.private,
)
}
val hasCookieBannerException =
if (requireContext().settings().shouldUseCookieBanner) {
withContext(Dispatchers.IO) {
cookieBannersStorage.hasException(
tab.content.url,
tab.content.private,
)
}
} else {
false
}
runIfFragmentIsAttached {
val isTrackingProtectionEnabled =
tab.trackingProtection.enabled && !hasTrackingProtectionException
@ -476,4 +490,22 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
internal fun updateLastBrowseActivity() {
requireContext().settings().lastBrowseActivity = System.currentTimeMillis()
}
private fun observeCookieBannerHandlingState(store: BrowserStore) {
consumeFlow(store) { flow ->
flow.mapNotNull { state ->
state.findCustomTabOrSelectedTab(customTabSessionId)
}.ifAnyChanged { tab ->
arrayOf(
tab.cookieBanner,
)
}.collect {
CookieBannerReEngagementDialogUtils.tryToShowReEngagementDialog(
settings = requireContext().settings(),
status = it.cookieBanner,
navController = findNavController(),
)
}
}
}
}

@ -22,8 +22,10 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.ReleaseChannel
import org.mozilla.fenix.components.metrics.AdjustMetricsService
import org.mozilla.fenix.components.metrics.DefaultMetricsStorage
import org.mozilla.fenix.components.metrics.GleanMetricsService
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.metrics.MetricsStorage
import org.mozilla.fenix.experiments.createNimbus
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.gleanplumb.CustomAttributeProvider
@ -31,6 +33,7 @@ import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage
import org.mozilla.fenix.gleanplumb.OnDiskMessageMetadataStorage
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.utils.BrowsersCache
import org.mozilla.geckoview.BuildConfig.MOZ_APP_BUILDID
import org.mozilla.geckoview.BuildConfig.MOZ_APP_VENDOR
import org.mozilla.geckoview.BuildConfig.MOZ_APP_VERSION
@ -115,6 +118,14 @@ class Analytics(
)
}
val metricsStorage: MetricsStorage by lazyMonitored {
DefaultMetricsStorage(
context = context,
settings = context.settings(),
checkDefaultBrowser = { BrowsersCache.all(context).isDefaultBrowser },
)
}
val metrics: MetricController by lazyMonitored {
MetricController.create(
listOf(

@ -31,5 +31,20 @@ sealed class Event {
* Event recording the first time Firefox is used 3 days in a row in the first week of install.
*/
object FirstWeekSeriesActivity : GrowthData("20ay7u")
/**
* Event recording that usage time has reached a threshold.
*/
object UsageThreshold : GrowthData("m66prt")
/**
* Event recording the first time Firefox has been resumed in a 24 hour period.
*/
object FirstAppOpenForDay : GrowthData("41hl22")
/**
* Event recording the first time a URI is loaded in Firefox in a 24 hour period.
*/
object FirstUriLoadForDay : GrowthData("ja86ek")
}
}

@ -6,6 +6,7 @@ package org.mozilla.fenix.components.metrics
import androidx.annotation.VisibleForTesting
import mozilla.components.browser.menu.facts.BrowserMenuFacts
import mozilla.components.browser.toolbar.facts.ToolbarFacts
import mozilla.components.concept.awesomebar.AwesomeBar
import mozilla.components.feature.autofill.facts.AutofillFacts
import mozilla.components.feature.awesomebar.facts.AwesomeBarFacts
@ -42,6 +43,7 @@ import org.mozilla.fenix.GleanMetrics.BrowserSearch
import org.mozilla.fenix.GleanMetrics.ContextMenu
import org.mozilla.fenix.GleanMetrics.ContextualMenu
import org.mozilla.fenix.GleanMetrics.CreditCards
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.LoginDialog
import org.mozilla.fenix.GleanMetrics.MediaNotification
import org.mozilla.fenix.GleanMetrics.MediaState
@ -147,6 +149,9 @@ internal class ReleaseMetricController(
else -> Unit
}
}
Component.BROWSER_TOOLBAR to ToolbarFacts.Items.MENU -> {
Events.toolbarMenuVisible.record(NoExtras())
}
Component.FEATURE_CONTEXTMENU to ContextMenuFacts.Items.ITEM -> {
metadata?.get("item")?.let { item ->
contextMenuAllowList[item]?.let { extraKey ->

@ -28,6 +28,7 @@ class MetricsMiddleware(
is AppAction.ResumedMetricsAction -> {
metrics.track(Event.GrowthData.SetAsDefault)
metrics.track(Event.GrowthData.FirstWeekSeriesActivity)
metrics.track(Event.GrowthData.UsageThreshold)
}
else -> Unit
}

@ -4,11 +4,14 @@
package org.mozilla.fenix.components.metrics
import android.app.Activity
import android.app.Application
import android.content.Context
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import mozilla.components.support.utils.ext.getPackageInfoCompat
import org.mozilla.fenix.android.DefaultActivityLifecycleCallbacks
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.utils.Settings
@ -29,6 +32,18 @@ interface MetricsStorage {
* Updates locally-stored state for an [event] that has just been sent.
*/
suspend fun updateSentState(event: Event)
/**
* Will try to register this as a recorder of app usage based on whether usage recording is still
* needed. It will measure usage by to monitoring lifecycle callbacks from [application]'s
* activities and should update local state using [updateUsageState].
*/
fun tryRegisterAsUsageRecorder(application: Application)
/**
* Update local state with a [usageLength] measurement.
*/
fun updateUsageState(usageLength: Long)
}
internal class DefaultMetricsStorage(
@ -62,6 +77,20 @@ internal class DefaultMetricsStorage(
Event.GrowthData.SerpAdClicked -> {
currentTime.duringFirstMonth() && !settings.adClickGrowthSent
}
Event.GrowthData.UsageThreshold -> {
!settings.usageTimeGrowthSent &&
settings.usageTimeGrowthData > usageThresholdMillis
}
Event.GrowthData.FirstAppOpenForDay -> {
currentTime.afterFirstDay() &&
currentTime.duringFirstMonth() &&
settings.resumeGrowthLastSent.hasBeenMoreThanDaySince()
}
Event.GrowthData.FirstUriLoadForDay -> {
currentTime.afterFirstDay() &&
currentTime.duringFirstMonth() &&
settings.uriLoadGrowthLastSent.hasBeenMoreThanDaySince()
}
}
}
@ -76,9 +105,29 @@ internal class DefaultMetricsStorage(
Event.GrowthData.SerpAdClicked -> {
settings.adClickGrowthSent = true
}
Event.GrowthData.UsageThreshold -> {
settings.usageTimeGrowthSent = true
}
Event.GrowthData.FirstAppOpenForDay -> {
settings.resumeGrowthLastSent = System.currentTimeMillis()
}
Event.GrowthData.FirstUriLoadForDay -> {
settings.uriLoadGrowthLastSent = System.currentTimeMillis()
}
}
}
override fun tryRegisterAsUsageRecorder(application: Application) {
// Currently there is only interest in measuring usage during the first day of install.
if (!settings.usageTimeGrowthSent && System.currentTimeMillis().duringFirstDay()) {
application.registerActivityLifecycleCallbacks(UsageRecorder(this))
}
}
override fun updateUsageState(usageLength: Long) {
settings.usageTimeGrowthData += usageLength
}
private fun updateDaysOfUse() {
val daysOfUse = settings.firstWeekDaysOfUseGrowthData
val currentDate = Calendar.getInstance(Locale.US)
@ -121,6 +170,12 @@ internal class DefaultMetricsStorage(
calendar.timeInMillis = this
}
private fun Long.hasBeenMoreThanDaySince() = System.currentTimeMillis() - this > dayMillis
private fun Long.afterFirstDay() = this > getInstalledTime() + dayMillis
private fun Long.duringFirstDay() = this < getInstalledTime() + dayMillis
private fun Long.duringFirstWeek() = this < getInstalledTime() + fullWeekMillis
private fun Long.duringFirstMonth() = this < getInstalledTime() + shortestMonthMillis
@ -129,6 +184,28 @@ internal class DefaultMetricsStorage(
calendar.add(Calendar.DAY_OF_MONTH, 1)
}
/**
* This will store app usage time to disk, based on Resume and Pause lifecycle events. Currently,
* there is only interest in usage during the first day after install.
*/
internal class UsageRecorder(
private val metricsStorage: MetricsStorage,
) : DefaultActivityLifecycleCallbacks {
private val activityStartTimes: MutableMap<String, Long?> = mutableMapOf()
override fun onActivityResumed(activity: Activity) {
super.onActivityResumed(activity)
activityStartTimes[activity.componentName.toString()] = System.currentTimeMillis()
}
override fun onActivityPaused(activity: Activity) {
super.onActivityPaused(activity)
val startTime = activityStartTimes[activity.componentName.toString()] ?: return
val elapsedTimeMillis = System.currentTimeMillis() - startTime
metricsStorage.updateUsageState(elapsedTimeMillis)
}
}
companion object {
private const val dayMillis: Long = 1000 * 60 * 60 * 24
private const val shortestMonthMillis: Long = dayMillis * 28
@ -137,6 +214,9 @@ internal class DefaultMetricsStorage(
// of the 7th day after install
private const val fullWeekMillis: Long = dayMillis * 8
// The usage threshold we are interested in is currently 340 seconds.
private const val usageThresholdMillis = 1000 * 340
/**
* Determines whether events should be tracked based on some general criteria:
* - user has installed as a result of a campaign

@ -11,12 +11,14 @@ import androidx.compose.material.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.navigation.findNavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.transformWhile
import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.lib.state.ext.flowScoped
@ -29,6 +31,7 @@ import org.mozilla.fenix.compose.cfr.CFRPopupProperties
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.settings.SupportUtils.SumoTopic.TOTAL_COOKIE_PROTECTION
import org.mozilla.fenix.settings.quicksettings.protections.cookiebanners.dialog.CookieBannerReEngagementDialogUtils
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.utils.Settings
@ -111,6 +114,7 @@ class BrowserToolbarCFRPresenter(
true -> TrackingProtection.tcpCfrExplicitDismissal.record(NoExtras())
false -> TrackingProtection.tcpCfrImplicitDismissal.record(NoExtras())
}
tryToShowCookieBannerDialogIfNeeded()
},
) {
Text(
@ -137,4 +141,15 @@ class BrowserToolbarCFRPresenter(
TrackingProtection.tcpCfrShown.record(NoExtras())
}
}
@VisibleForTesting
internal fun tryToShowCookieBannerDialogIfNeeded() {
browserStore.state.selectedTab?.let { tab ->
CookieBannerReEngagementDialogUtils.tryToShowReEngagementDialog(
settings = settings,
status = tab.cookieBanner,
navController = toolbar.findNavController(),
)
}
}
}

@ -42,6 +42,7 @@ abstract class ToolbarIntegration(
toolbar,
store,
sessionId,
context.settings().showUnifiedSearchFeature,
ToolbarFeature.UrlRenderConfiguration(
context.components.publicSuffixList,
ThemeManager.resolveAttribute(R.attr.textPrimary, context),

@ -4,7 +4,6 @@
package org.mozilla.fenix.compose
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -16,8 +15,8 @@ import androidx.compose.foundation.layout.width
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.theme.FirefoxTheme
/**
@ -39,8 +38,7 @@ fun Divider(
* An example of a vertical divider.
*/
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@LightDarkPreview
private fun VerticalDividerPreview() {
FirefoxTheme {
Box(
@ -74,8 +72,7 @@ private fun VerticalDividerPreview() {
* An example of divider usage in a list menu.
*/
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@LightDarkPreview
private fun HorizontalDividerPreview() {
FirefoxTheme {
Box(

@ -4,7 +4,6 @@
package org.mozilla.fenix.compose
import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -22,9 +21,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.theme.FirefoxTheme
/**
@ -167,8 +166,7 @@ fun ListItemTabSurface(
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@LightDarkPreview
private fun ListItemTabLargePreview() {
FirefoxTheme {
ListItemTabLarge(
@ -180,8 +178,7 @@ private fun ListItemTabLargePreview() {
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@LightDarkPreview
private fun ListItemTabSurfacePreview() {
FirefoxTheme {
ListItemTabSurface(
@ -197,8 +194,7 @@ private fun ListItemTabSurfacePreview() {
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@LightDarkPreview
private fun ListItemTabSurfaceWithCustomBackgroundPreview() {
FirefoxTheme {
ListItemTabSurface(

@ -4,7 +4,6 @@
package org.mozilla.fenix.compose
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
@ -26,10 +25,10 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.compose.button.PrimaryButton
import org.mozilla.fenix.theme.FirefoxTheme
@ -199,8 +198,7 @@ data class MessageCardColors(
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@LightDarkPreview
private fun MessageCardPreview() {
FirefoxTheme {
Box(
@ -219,8 +217,7 @@ private fun MessageCardPreview() {
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@LightDarkPreview
private fun MessageCardWithoutTitlePreview() {
FirefoxTheme {
Box(
@ -238,8 +235,7 @@ private fun MessageCardWithoutTitlePreview() {
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@LightDarkPreview
private fun MessageCardWithButtonLabelPreview() {
FirefoxTheme {
Box(

@ -25,6 +25,7 @@ import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.theme.FirefoxTheme
/**
@ -74,8 +75,7 @@ fun SelectableChip(
}
@Composable
@Preview(uiMode = UI_MODE_NIGHT_YES)
@Preview(uiMode = UI_MODE_NIGHT_NO)
@LightDarkPreview
private fun SelectableChipPreview() {
FirefoxTheme {
Row(

@ -0,0 +1,16 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.compose.annotation
import android.content.res.Configuration
import androidx.compose.ui.tooling.preview.Preview
/**
* A wrapper annotation for the two uiMode that are commonly used
* in Compose preview functions.
*/
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
annotation class LightDarkPreview

@ -4,7 +4,6 @@
package org.mozilla.fenix.compose.button
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -21,9 +20,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.theme.FirefoxTheme
/**
@ -183,8 +182,7 @@ fun DestructiveButton(
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@LightDarkPreview
private fun ButtonPreview() {
FirefoxTheme {
Column(

@ -4,14 +4,13 @@
package org.mozilla.fenix.compose.button
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.theme.FirefoxTheme
import java.util.Locale
@ -44,8 +43,7 @@ fun TextButton(
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@LightDarkPreview
private fun TextButtonPreview() {
FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer1)) {

@ -4,7 +4,6 @@
package org.mozilla.fenix.compose.tabstray
import android.content.res.Configuration
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
@ -14,13 +13,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.accompanist.drawablepainter.rememberDrawablePainter
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createTab
import mozilla.components.concept.engine.mediasession.MediaSession.PlaybackState
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.theme.FirefoxTheme
/**
@ -55,8 +54,7 @@ fun MediaImage(
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@LightDarkPreview
private fun ImagePreview() {
FirefoxTheme {
MediaImage(

@ -4,7 +4,6 @@
package org.mozilla.fenix.compose.tabstray
import android.content.res.Configuration
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
@ -38,7 +37,6 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.text.BidiFormatter
@ -49,6 +47,7 @@ import org.mozilla.fenix.compose.Divider
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.theme.FirefoxTheme
/**
@ -231,8 +230,7 @@ private fun Thumbnail(
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@LightDarkPreview
private fun TabGridItemPreview() {
FirefoxTheme {
TabGridItem(
@ -249,8 +247,7 @@ private fun TabGridItemPreview() {
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@LightDarkPreview
private fun TabGridItemSelectedPreview() {
FirefoxTheme {
TabGridItem(
@ -265,8 +262,7 @@ private fun TabGridItemSelectedPreview() {
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@LightDarkPreview
private fun TabGridItemMultiSelectedPreview() {
FirefoxTheme {
TabGridItem(

@ -4,7 +4,6 @@
package org.mozilla.fenix.compose.tabstray
import android.content.res.Configuration
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
@ -25,13 +24,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createTab
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.theme.FirefoxTheme
@ -168,8 +167,7 @@ private fun Thumbnail(
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@LightDarkPreview
private fun TabListItemPreview() {
FirefoxTheme {
TabListItem(
@ -183,8 +181,7 @@ private fun TabListItemPreview() {
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@LightDarkPreview
private fun SelectedTabListItemPreview() {
FirefoxTheme {
TabListItem(

@ -167,8 +167,11 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
val cookieBannersStorage = requireComponents.core.cookieBannersStorage
requireComponents.useCases.trackingProtectionUseCases.containsException(tab.id) { contains ->
lifecycleScope.launch(Dispatchers.IO) {
val hasException =
val hasException = if (requireContext().settings().shouldUseCookieBanner) {
cookieBannersStorage.hasException(tab.content.url, tab.content.private)
} else {
false
}
withContext(Dispatchers.Main) {
runIfFragmentIsAttached {
val directions = ExternalAppBrowserFragmentDirections

@ -0,0 +1,60 @@
/* 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.ext
import android.os.Build
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationManagerCompat
/**
* Returns whether notifications are enabled, catches any exception that was thrown from
* [NotificationManagerCompat.areNotificationsEnabled] and returns false.
*/
@Suppress("TooGenericExceptionCaught")
fun NotificationManagerCompat.areNotificationsEnabledSafe(): Boolean {
return try {
areNotificationsEnabled()
} catch (e: Exception) {
false
}
}
/**
* If the channel does not exist or is null, this returns false.
* If the channel exists with importance more than [NotificationManagerCompat.IMPORTANCE_NONE] and
* notifications are enabled for the app, this returns true.
* On <= SDK 26, this checks if notifications are enabled for the app.
*
* @param channelId the id of the notification channel to check.
* @return true if the channel is enabled, false otherwise.
*/
fun NotificationManagerCompat.isNotificationChannelEnabled(channelId: String): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = getNotificationChannelSafe(channelId)
if (channel == null) {
false
} else {
areNotificationsEnabledSafe() && channel.importance != NotificationManagerCompat.IMPORTANCE_NONE
}
} else {
areNotificationsEnabledSafe()
}
}
/**
* Returns the notification channel with the given [channelId], or null if the channel does not
* exist, catches any exception that was thrown by
* [NotificationManagerCompat.getNotificationChannelCompat] and returns null.
*
* @param channelId the id of the notification channel to check.
*/
@Suppress("TooGenericExceptionCaught")
private fun NotificationManagerCompat.getNotificationChannelSafe(channelId: String): NotificationChannelCompat? {
return try {
getNotificationChannelCompat(channelId)
} catch (e: Exception) {
null
}
}

@ -362,7 +362,7 @@ class HomeFragment : Fragment() {
}
UnifiedSearch.searchMenuTapped.record(NoExtras())
searchSelectorMenu.menuController.show(anchor = it, orientation = orientation, forceOrientation = true)
searchSelectorMenu.menuController.show(anchor = it, orientation = orientation)
}
}
@ -418,7 +418,6 @@ class HomeFragment : Fragment() {
pocketStoriesController = DefaultPocketStoriesController(
homeActivity = activity,
appStore = components.appStore,
navController = findNavController(),
),
)

@ -62,17 +62,15 @@ private val expandedCollectionShape = RoundedCornerShape(topStart = 8.dp, topEnd
* @param menuItems List of [CollectionMenuItem] to be shown in a menu.
* @param onToggleCollectionExpanded Invoked when the user clicks on the collection.
* @param onCollectionShareTabsClicked Invoked when the user clicks to share the collection.
* @param onCollectionMenuOpened Invoked when the user clicks to open a menu for the collection.
*/
@Composable
@Suppress("LongParameterList", "LongMethod")
@Suppress("LongMethod")
fun Collection(
collection: TabCollection,
expanded: Boolean,
menuItems: List<CollectionMenuItem>,
onToggleCollectionExpanded: (TabCollection, Boolean) -> Unit,
onCollectionShareTabsClicked: (TabCollection) -> Unit,
onCollectionMenuOpened: () -> Unit,
) {
var isMenuExpanded by remember(collection) { mutableStateOf(false) }
val isExpanded by remember(collection) { mutableStateOf(expanded) }
@ -131,7 +129,6 @@ fun Collection(
IconButton(
onClick = {
isMenuExpanded = !isMenuExpanded
onCollectionMenuOpened()
},
) {
Icon(
@ -165,7 +162,6 @@ private fun CollectionDarkPreview() {
menuItems = emptyList(),
onToggleCollectionExpanded = { _, _ -> },
onCollectionShareTabsClicked = {},
onCollectionMenuOpened = {},
)
}
}
@ -180,7 +176,6 @@ private fun CollectionDarkExpandedPreview() {
menuItems = emptyList(),
onToggleCollectionExpanded = { _, _ -> },
onCollectionShareTabsClicked = {},
onCollectionMenuOpened = {},
)
}
}
@ -195,7 +190,6 @@ private fun CollectionLightPreview() {
menuItems = emptyList(),
onToggleCollectionExpanded = { _, _ -> },
onCollectionShareTabsClicked = {},
onCollectionMenuOpened = {},
)
}
}
@ -210,7 +204,6 @@ private fun CollectionLightExpandedPreview() {
menuItems = emptyList(),
onToggleCollectionExpanded = { _, _ -> },
onCollectionShareTabsClicked = {},
onCollectionMenuOpened = {},
)
}
}

@ -5,7 +5,6 @@
package org.mozilla.fenix.home.collections
import android.content.Context
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -35,7 +34,6 @@ import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
import mozilla.components.browser.state.state.recover.RecoverableTab
@ -43,6 +41,7 @@ import mozilla.components.concept.engine.Engine
import mozilla.components.feature.tab.collections.Tab
import org.mozilla.fenix.R.drawable
import org.mozilla.fenix.R.string
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.compose.list.FaviconListItem
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.theme.FirefoxTheme
@ -191,8 +190,7 @@ private fun Modifier.clipTop() = this.then(
)
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@LightDarkPreview
private fun TabInCollectionPreview() {
FirefoxTheme {
Column {

@ -67,7 +67,6 @@ class CollectionViewHolder(
menuItems = menuItems,
onToggleCollectionExpanded = interactor::onToggleCollectionExpanded,
onCollectionShareTabsClicked = interactor::onCollectionShareTabsClicked,
onCollectionMenuOpened = interactor::onCollectionMenuOpened,
)
}
}

@ -0,0 +1,40 @@
/* 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 androidx.navigation.navOptions
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.MetricsUtils
import org.mozilla.fenix.ext.nav
/**
* Long pressing home button should also open to the search fragment if Fenix is set as the
* assist app
*/
class AssistIntentProcessor : HomeIntentProcessor {
override fun process(intent: Intent, navController: NavController, out: Intent): Boolean {
if (intent.action != Intent.ACTION_ASSIST) {
return false
}
val directions = NavGraphDirections.actionGlobalSearchDialog(
sessionId = null,
// Will follow this up with adding `ASSIST` as a search source.
// https://bugzilla.mozilla.org/show_bug.cgi?id=1808043
searchAccessPoint = MetricsUtils.Source.NONE,
)
val options = navOptions {
popUpTo(R.id.homeFragment)
}
navController.nav(null, directions, options)
return true
}
}

@ -4,7 +4,6 @@
package org.mozilla.fenix.home.pocket
import android.content.res.Configuration
import android.view.View
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column
@ -15,7 +14,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView
@ -23,6 +21,7 @@ import mozilla.components.lib.state.ext.observeAsComposableState
import org.mozilla.fenix.R
import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.ComposeViewHolder
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.compose.home.HomeSectionHeader
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.wallpapers.WallpaperState
@ -127,8 +126,7 @@ private fun PocketTopics(
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@LightDarkPreview
private fun PocketCategoriesViewHolderPreview() {
FirefoxTheme {
PocketTopics(

@ -6,7 +6,6 @@
package org.mozilla.fenix.home.pocket
import android.content.res.Configuration
import android.view.View
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@ -18,7 +17,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView
@ -26,6 +24,7 @@ import mozilla.components.lib.state.ext.observeAsComposableState
import org.mozilla.fenix.R
import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.ComposeViewHolder
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.theme.FirefoxTheme
/**
@ -79,8 +78,7 @@ class PocketRecommendationsHeaderViewHolder(
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@LightDarkPreview
private fun PocketRecommendationsFooterViewHolderPreview() {
FirefoxTheme {
Box(modifier = Modifier.background(color = FirefoxTheme.colors.layer1)) {

@ -4,8 +4,6 @@
package org.mozilla.fenix.home.pocket
import androidx.annotation.VisibleForTesting
import androidx.navigation.NavController
import mozilla.components.service.glean.private.NoExtras
import mozilla.components.service.pocket.PocketStory
import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
@ -15,7 +13,6 @@ import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.Pings
import org.mozilla.fenix.GleanMetrics.Pocket
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction
@ -73,12 +70,10 @@ interface PocketStoriesController {
*
* @param homeActivity [HomeActivity] used to open URLs in a new tab.
* @param appStore [AppStore] from which to read the current Pocket recommendations and dispatch new actions on.
* @param navController [NavController] used for navigation.
*/
internal class DefaultPocketStoriesController(
private val homeActivity: HomeActivity,
private val appStore: AppStore,
private val navController: NavController,
) : PocketStoriesController {
override fun handleStoryShown(
storyShown: PocketStory,
@ -153,7 +148,6 @@ internal class DefaultPocketStoriesController(
storyClicked: PocketStory,
storyPosition: Pair<Int, Int>,
) {
dismissSearchDialogIfDisplayed()
homeActivity.openToBrowserAndLoad(storyClicked.url, true, BrowserDirection.FromHome)
when (storyClicked) {
@ -179,21 +173,12 @@ internal class DefaultPocketStoriesController(
}
override fun handleLearnMoreClicked(link: String) {
dismissSearchDialogIfDisplayed()
homeActivity.openToBrowserAndLoad(link, true, BrowserDirection.FromHome)
Pocket.homeRecsLearnMoreClicked.record(NoExtras())
}
override fun handleDiscoverMoreClicked(link: String) {
dismissSearchDialogIfDisplayed()
homeActivity.openToBrowserAndLoad(link, true, BrowserDirection.FromHome)
Pocket.homeRecsDiscoverClicked.record(NoExtras())
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun dismissSearchDialogIfDisplayed() {
if (navController.currentDestination?.id == R.id.searchDialogFragment) {
navController.navigateUp()
}
}
}

@ -4,8 +4,6 @@
package org.mozilla.fenix.home.recentbookmarks.controller
import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.Companion.PRIVATE
import androidx.navigation.NavController
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.concept.engine.EngineSession
@ -13,7 +11,6 @@ import mozilla.components.concept.engine.EngineSession.LoadUrlFlags.Companion.AL
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.RecentBookmarks
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.home.HomeFragmentDirections
@ -40,11 +37,6 @@ interface RecentBookmarksController {
* @see [RecentBookmarksInteractor.onRecentBookmarkRemoved]
*/
fun handleBookmarkRemoved(bookmark: RecentBookmark)
/**
* @see [RecentBookmarksInteractor.onRecentBookmarkLongClicked]
*/
fun handleBookmarkLongClicked()
}
/**
@ -57,7 +49,6 @@ class DefaultRecentBookmarksController(
) : RecentBookmarksController {
override fun handleBookmarkClicked(bookmark: RecentBookmark) {
dismissSearchDialogIfDisplayed()
activity.openToBrowserAndLoad(
searchTermOrURL = bookmark.url!!,
newTab = true,
@ -69,7 +60,6 @@ class DefaultRecentBookmarksController(
override fun handleShowAllBookmarksClicked() {
RecentBookmarks.showAllBookmarks.add()
dismissSearchDialogIfDisplayed()
navController.navigate(
HomeFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id),
)
@ -78,15 +68,4 @@ class DefaultRecentBookmarksController(
override fun handleBookmarkRemoved(bookmark: RecentBookmark) {
appStore.dispatch(AppAction.RemoveRecentBookmark(bookmark))
}
override fun handleBookmarkLongClicked() {
dismissSearchDialogIfDisplayed()
}
@VisibleForTesting(otherwise = PRIVATE)
fun dismissSearchDialogIfDisplayed() {
if (navController.currentDestination?.id == R.id.searchDialogFragment) {
navController.navigateUp()
}
}
}

@ -33,9 +33,4 @@ interface RecentBookmarksInteractor {
* @param bookmark The bookmark that has been removed.
*/
fun onRecentBookmarkRemoved(bookmark: RecentBookmark)
/**
* Called when the user long clicks a recent bookmark.
*/
fun onRecentBookmarkLongClicked()
}

@ -4,7 +4,6 @@
package org.mozilla.fenix.home.recentbookmarks.view
import android.content.res.Configuration
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@ -43,7 +42,6 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import mozilla.components.browser.icons.compose.Loader
import mozilla.components.browser.icons.compose.Placeholder
@ -51,6 +49,7 @@ import mozilla.components.browser.icons.compose.WithIcon
import mozilla.components.ui.colors.PhotonColors
import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.Image
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.compose.inComposePreview
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
import org.mozilla.fenix.theme.FirefoxTheme
@ -70,7 +69,6 @@ private val imageModifier = Modifier
* @param menuItems List of [RecentBookmarksMenuItem] shown when long clicking a [RecentBookmarkItem]
* @param backgroundColor The background [Color] of each bookmark.
* @param onRecentBookmarkClick Invoked when the user clicks on a recent bookmark.
* @param onRecentBookmarkLongClick Invoked when the user long clicks on a recent bookmark.
*/
@OptIn(ExperimentalComposeUiApi::class)
@Composable
@ -79,7 +77,6 @@ fun RecentBookmarks(
menuItems: List<RecentBookmarksMenuItem>,
backgroundColor: Color,
onRecentBookmarkClick: (RecentBookmark) -> Unit = {},
onRecentBookmarkLongClick: () -> Unit = {},
) {
LazyRow(
modifier = Modifier.semantics {
@ -95,7 +92,6 @@ fun RecentBookmarks(
menuItems = menuItems,
backgroundColor = backgroundColor,
onRecentBookmarkClick = onRecentBookmarkClick,
onRecentBookmarkLongClick = onRecentBookmarkLongClick,
)
}
}
@ -108,7 +104,6 @@ fun RecentBookmarks(
* @param menuItems The list of [RecentBookmarksMenuItem] shown when long clicking on the recent bookmark item.
* @param backgroundColor The background [Color] of the recent bookmark item.
* @param onRecentBookmarkClick Invoked when the user clicks on the recent bookmark item.
* @param onRecentBookmarkLongClick Invoked when the user long clicks on the recent bookmark item.
*/
@OptIn(
ExperimentalFoundationApi::class,
@ -120,7 +115,6 @@ private fun RecentBookmarkItem(
menuItems: List<RecentBookmarksMenuItem>,
backgroundColor: Color,
onRecentBookmarkClick: (RecentBookmark) -> Unit = {},
onRecentBookmarkLongClick: () -> Unit = {},
) {
var isMenuExpanded by remember { mutableStateOf(false) }
@ -130,10 +124,7 @@ private fun RecentBookmarkItem(
.combinedClickable(
enabled = true,
onClick = { onRecentBookmarkClick(bookmark) },
onLongClick = {
onRecentBookmarkLongClick()
isMenuExpanded = true
},
onLongClick = { isMenuExpanded = true },
),
shape = cardShape,
backgroundColor = backgroundColor,
@ -276,8 +267,7 @@ private fun RecentBookmarksMenu(
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@LightDarkPreview
private fun RecentBookmarksPreview() {
FirefoxTheme {
RecentBookmarks(

@ -14,7 +14,6 @@ import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.LifecycleOwner
import androidx.navigation.findNavController
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.ComposeViewHolder
import org.mozilla.fenix.compose.home.HomeSectionHeader
@ -38,13 +37,6 @@ class RecentBookmarksHeaderViewHolder(
composeView.setPadding(horizontalPadding, 0, horizontalPadding, 0)
}
private fun dismissSearchDialogIfDisplayed() {
val navController = itemView.findNavController()
if (navController.currentDestination?.id == R.id.searchDialogFragment) {
navController.navigateUp()
}
}
@Composable
override fun Content() {
Column {
@ -54,7 +46,6 @@ class RecentBookmarksHeaderViewHolder(
headerText = stringResource(R.string.recently_saved_title),
description = stringResource(R.string.recently_saved_show_all_content_description_2),
onShowAllClick = {
dismissSearchDialogIfDisplayed()
interactor.onShowAllBookmarksClicked()
},
)

@ -48,7 +48,6 @@ class RecentBookmarksViewHolder(
onClick = { bookmark -> interactor.onRecentBookmarkRemoved(bookmark) },
),
),
onRecentBookmarkLongClick = interactor::onRecentBookmarkLongClicked,
)
}
}

@ -25,11 +25,6 @@ interface RecentSyncedTabController {
*/
fun handleRecentSyncedTabClick(tab: RecentSyncedTab)
/**
* @see [RecentSyncedTabInteractor.onRecentSyncedTabLongClick]
*/
fun handleRecentSyncedTabLongClick()
/**
* @see [RecentSyncedTabInteractor.onRecentSyncedTabClicked]
*/
@ -71,12 +66,6 @@ class DefaultRecentSyncedTabController(
)
}
override fun handleRecentSyncedTabLongClick() {
if (navController.currentDestination?.id == R.id.searchDialogFragment) {
navController.navigateUp()
}
}
override fun handleRecentSyncedTabRemoved(tab: RecentSyncedTab) {
appStore.dispatch(AppAction.RemoveRecentSyncedTab(tab))
}

@ -17,11 +17,6 @@ interface RecentSyncedTabInteractor {
*/
fun onRecentSyncedTabClicked(tab: RecentSyncedTab)
/**
* Called when opening the dropdown menu on a recent synced tab by long press.
*/
fun onRecentSyncedTabLongClick()
/**
* Opens the tabs tray to the synced tab page. Called when a user clicks on the "See all synced
* tabs" button.

@ -63,7 +63,6 @@ import org.mozilla.fenix.theme.FirefoxTheme
* @param onRecentSyncedTabClick Invoked when the user clicks on the recent synced tab.
* @param onSeeAllSyncedTabsButtonClick Invoked when user clicks on the "See all" button in the synced tab card.
* @param onRemoveSyncedTab Invoked when user clicks on the "Remove" dropdown menu option.
* @param onRecentSyncedTabLongClick Invoked when user long presses the recent synced tab.
*/
@OptIn(ExperimentalFoundationApi::class)
@Suppress("LongMethod", "LongParameterList")
@ -76,7 +75,6 @@ fun RecentSyncedTab(
onRecentSyncedTabClick: (RecentSyncedTab) -> Unit,
onSeeAllSyncedTabsButtonClick: () -> Unit,
onRemoveSyncedTab: (RecentSyncedTab) -> Unit,
onRecentSyncedTabLongClick: () -> Unit,
) {
var isDropdownExpanded by remember { mutableStateOf(false) }
@ -91,10 +89,7 @@ fun RecentSyncedTab(
.height(180.dp)
.combinedClickable(
onClick = { tab?.let { onRecentSyncedTabClick(tab) } },
onLongClick = {
onRecentSyncedTabLongClick()
isDropdownExpanded = true
},
onLongClick = { isDropdownExpanded = true },
),
shape = RoundedCornerShape(8.dp),
backgroundColor = backgroundColor,
@ -292,7 +287,6 @@ private fun LoadedRecentSyncedTab() {
onRecentSyncedTabClick = {},
onSeeAllSyncedTabsButtonClick = {},
onRemoveSyncedTab = {},
onRecentSyncedTabLongClick = {},
)
}
}
@ -307,7 +301,6 @@ private fun LoadingRecentSyncedTab() {
onRecentSyncedTabClick = {},
onSeeAllSyncedTabsButtonClick = {},
onRemoveSyncedTab = {},
onRecentSyncedTabLongClick = {},
)
}
}

@ -77,7 +77,6 @@ class RecentSyncedTabViewHolder(
onRecentSyncedTabClick = recentSyncedTabInteractor::onRecentSyncedTabClicked,
onSeeAllSyncedTabsButtonClick = recentSyncedTabInteractor::onSyncedTabShowAllClicked,
onRemoveSyncedTab = recentSyncedTabInteractor::onRemovedRecentSyncedTab,
onRecentSyncedTabLongClick = recentSyncedTabInteractor::onRecentSyncedTabLongClick,
)
}
}

@ -4,8 +4,6 @@
package org.mozilla.fenix.home.recenttabs.controller
import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.Companion.PRIVATE
import androidx.navigation.NavController
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.tabs.TabsUseCases.SelectTabUseCase
@ -29,11 +27,6 @@ interface RecentTabController {
*/
fun handleRecentTabClicked(tabId: String)
/**
* @see [RecentTabInteractor.onRecentTabLongClicked]
*/
fun handleRecentTabLongClicked()
/**
* @see [RecentTabInteractor.onRecentTabShowAllClicked]
*/
@ -69,12 +62,7 @@ class DefaultRecentTabsController(
navController.navigate(R.id.browserFragment)
}
override fun handleRecentTabLongClicked() {
dismissSearchDialogIfDisplayed()
}
override fun handleRecentTabShowAllClicked() {
dismissSearchDialogIfDisplayed()
RecentTabs.showAllClicked.record(NoExtras())
navController.navigate(HomeFragmentDirections.actionGlobalTabsTrayFragment())
}
@ -82,11 +70,4 @@ class DefaultRecentTabsController(
override fun handleRecentTabRemoved(tab: RecentTab.Tab) {
appStore.dispatch(AppAction.RemoveRecentTab(tab))
}
@VisibleForTesting(otherwise = PRIVATE)
fun dismissSearchDialogIfDisplayed() {
if (navController.currentDestination?.id == R.id.searchDialogFragment) {
navController.navigateUp()
}
}
}

@ -17,11 +17,6 @@ interface RecentTabInteractor {
*/
fun onRecentTabClicked(tabId: String)
/**
* Called when the user long clicks on a recent tab.
*/
fun onRecentTabLongClicked()
/**
* Show the tabs tray. Called when a user clicks on the "Show all" button besides the recent
* tabs.

@ -50,7 +50,6 @@ class RecentTabViewHolder(
recentTabs = recentTabs.value ?: emptyList(),
backgroundColor = wallpaperState.wallpaperCardColor,
onRecentTabClick = { recentTabInteractor.onRecentTabClicked(it) },
onRecentTabLongClick = { recentTabInteractor.onRecentTabLongClicked() },
menuItems = listOf(
RecentTabMenuItem(
title = stringResource(id = R.string.recent_tab_menu_item_remove),

@ -6,7 +6,6 @@
package org.mozilla.fenix.home.recenttabs.view
import android.content.res.Configuration
import android.graphics.Bitmap
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
@ -49,7 +48,6 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import mozilla.components.browser.icons.compose.Loader
@ -62,6 +60,7 @@ import mozilla.components.ui.colors.PhotonColors
import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.Image
import org.mozilla.fenix.compose.ThumbnailCard
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.compose.inComposePreview
import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.theme.FirefoxTheme
@ -81,7 +80,6 @@ fun RecentTabs(
menuItems: List<RecentTabMenuItem>,
backgroundColor: Color = FirefoxTheme.colors.layer2,
onRecentTabClick: (String) -> Unit = {},
onRecentTabLongClick: () -> Unit = {},
) {
Column(
modifier = Modifier
@ -100,7 +98,6 @@ fun RecentTabs(
menuItems = menuItems,
backgroundColor = backgroundColor,
onRecentTabClick = onRecentTabClick,
onRecentTabLongClick = onRecentTabLongClick,
)
}
}
@ -126,7 +123,6 @@ private fun RecentTabItem(
menuItems: List<RecentTabMenuItem>,
backgroundColor: Color,
onRecentTabClick: (String) -> Unit = {},
onRecentTabLongClick: () -> Unit = {},
) {
var isMenuExpanded by remember { mutableStateOf(false) }
@ -137,10 +133,7 @@ private fun RecentTabItem(
.combinedClickable(
enabled = true,
onClick = { onRecentTabClick(tab.state.id) },
onLongClick = {
onRecentTabLongClick()
isMenuExpanded = true
},
onLongClick = { isMenuExpanded = true },
),
shape = RoundedCornerShape(8.dp),
backgroundColor = backgroundColor,
@ -363,8 +356,7 @@ private fun PlaceHolderTabIcon(modifier: Modifier) {
)
}
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@LightDarkPreview
@Composable
private fun RecentTabsPreview() {
val tab = RecentTab.Tab(

@ -14,7 +14,6 @@ import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.LifecycleOwner
import androidx.navigation.findNavController
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.ComposeViewHolder
import org.mozilla.fenix.compose.home.HomeSectionHeader
@ -37,13 +36,6 @@ class RecentTabsHeaderViewHolder(
composeView.setPadding(horizontalPadding, 0, horizontalPadding, 0)
}
private fun dismissSearchDialogIfDisplayed() {
val navController = itemView.findNavController()
if (navController.currentDestination?.id == R.id.searchDialogFragment) {
navController.navigateUp()
}
}
@Composable
override fun Content() {
Column {
@ -53,7 +45,6 @@ class RecentTabsHeaderViewHolder(
headerText = stringResource(R.string.recent_tabs_header),
description = stringResource(R.string.recent_tabs_show_all_content_description_2),
onShowAllClick = {
dismissSearchDialogIfDisplayed()
interactor.onRecentTabShowAllClicked()
},
)

@ -4,8 +4,6 @@
package org.mozilla.fenix.home.recentvisits.controller
import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.Companion.PRIVATE
import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -60,11 +58,6 @@ interface RecentVisitsController {
* @param highlightUrl Url of the [RecentHistoryHighlight] to remove.
*/
fun handleRemoveRecentHistoryHighlight(highlightUrl: String)
/**
* Callback for when the user long clicks on a recent visit.
*/
fun handleRecentVisitLongClicked()
}
/**
@ -83,7 +76,6 @@ class DefaultRecentVisitsController(
* Shows the history fragment.
*/
override fun handleHistoryShowAllClicked() {
dismissSearchDialogIfDisplayed()
navController.navigate(
HomeFragmentDirections.actionGlobalHistoryFragment(),
)
@ -144,18 +136,4 @@ class DefaultRecentVisitsController(
storage.deleteHistoryMetadataForUrl(highlightUrl)
}
}
/**
* Dismiss the search dialog if displayed.
*/
override fun handleRecentVisitLongClicked() {
dismissSearchDialogIfDisplayed()
}
@VisibleForTesting(otherwise = PRIVATE)
fun dismissSearchDialogIfDisplayed() {
if (navController.currentDestination?.id == R.id.searchDialogFragment) {
navController.navigateUp()
}
}
}

@ -44,9 +44,4 @@ interface RecentVisitsInteractor {
* @param highlightUrl [RecentHistoryHighlight.url] of the item to remove.
*/
fun onRemoveRecentHistoryHighlight(highlightUrl: String)
/**
* Called when opening the dropdown menu on a recent visit by long press.
*/
fun onRecentVisitLongClicked()
}

@ -70,7 +70,6 @@ private const val VISITS_PER_COLUMN = 3
* @param menuItems List of [RecentVisitMenuItem] shown long clicking a [RecentlyVisitedItem].
* @param backgroundColor The background [Color] of each item.
* @param onRecentVisitClick Invoked when the user clicks on a recent visit.
* @param onRecentVisitLongClick Invoked when the user long clicks on a recent visit.
*/
@OptIn(ExperimentalComposeUiApi::class)
@Composable
@ -79,7 +78,6 @@ fun RecentlyVisited(
menuItems: List<RecentVisitMenuItem>,
backgroundColor: Color = FirefoxTheme.colors.layer2,
onRecentVisitClick: (RecentlyVisitedItem, Int) -> Unit = { _, _ -> },
onRecentVisitLongClick: () -> Unit = {},
) {
Card(
modifier = Modifier.fillMaxWidth(),
@ -116,7 +114,6 @@ fun RecentlyVisited(
onRecentVisitClick = {
onRecentVisitClick(it, pageIndex + 1)
},
onRecentVisitLongClick = { onRecentVisitLongClick() },
)
is RecentHistoryGroup -> RecentlyVisitedHistoryGroup(
recentVisit = recentVisit,
@ -126,7 +123,6 @@ fun RecentlyVisited(
onRecentVisitClick = {
onRecentVisitClick(it, pageIndex + 1)
},
onRecentVisitLongClick = { onRecentVisitLongClick() },
)
}
}
@ -144,13 +140,11 @@ fun RecentlyVisited(
* @param clickableEnabled Whether click actions should be invoked or not.
* @param showDividerLine Whether to show a divider line at the bottom.
* @param onRecentVisitClick Invoked when the user clicks on a recent visit.
* @param onRecentVisitClick Invoked when the user long clicks on a recently visited group.
*/
@OptIn(
ExperimentalFoundationApi::class,
ExperimentalComposeUiApi::class,
)
@Suppress("LongParameterList")
@Composable
private fun RecentlyVisitedHistoryGroup(
recentVisit: RecentHistoryGroup,
@ -158,7 +152,6 @@ private fun RecentlyVisitedHistoryGroup(
clickableEnabled: Boolean,
showDividerLine: Boolean,
onRecentVisitClick: (RecentHistoryGroup) -> Unit = { _ -> },
onRecentVisitLongClick: () -> Unit = {},
) {
var isMenuExpanded by remember { mutableStateOf(false) }
@ -167,10 +160,7 @@ private fun RecentlyVisitedHistoryGroup(
.combinedClickable(
enabled = clickableEnabled,
onClick = { onRecentVisitClick(recentVisit) },
onLongClick = {
onRecentVisitLongClick()
isMenuExpanded = true
},
onLongClick = { isMenuExpanded = true },
)
.size(268.dp, 56.dp)
.semantics {
@ -233,13 +223,11 @@ private fun RecentlyVisitedHistoryGroup(
* @param clickableEnabled Whether click actions should be invoked or not.
* @param showDividerLine Whether to show a divider line at the bottom.
* @param onRecentVisitClick Invoked when the user clicks on a recent visit.
* @param onRecentVisitLongClick Invoked when the user long clicks on a recent visit highlight.
*/
@OptIn(
ExperimentalFoundationApi::class,
ExperimentalComposeUiApi::class,
)
@Suppress("LongParameterList")
@Composable
private fun RecentlyVisitedHistoryHighlight(
recentVisit: RecentHistoryHighlight,
@ -247,7 +235,6 @@ private fun RecentlyVisitedHistoryHighlight(
clickableEnabled: Boolean,
showDividerLine: Boolean,
onRecentVisitClick: (RecentHistoryHighlight) -> Unit = { _ -> },
onRecentVisitLongClick: () -> Unit = {},
) {
var isMenuExpanded by remember { mutableStateOf(false) }
@ -256,10 +243,7 @@ private fun RecentlyVisitedHistoryHighlight(
.combinedClickable(
enabled = clickableEnabled,
onClick = { onRecentVisitClick(recentVisit) },
onLongClick = {
onRecentVisitLongClick()
isMenuExpanded = true
},
onLongClick = { isMenuExpanded = true },
)
.size(268.dp, 56.dp)
.semantics {

@ -80,7 +80,6 @@ class RecentlyVisitedViewHolder(
}
}
},
onRecentVisitLongClick = { interactor.onRecentVisitLongClicked() },
)
}

@ -41,7 +41,6 @@ import org.mozilla.fenix.GleanMetrics.TopSites
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserAnimator
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.collections.SaveCollectionStep
import org.mozilla.fenix.components.AppStore
@ -178,11 +177,6 @@ interface SessionControlController {
*/
fun handleRemoveCollectionsPlaceholder()
/**
* @see [CollectionInteractor.onCollectionMenuOpened] and [TopSiteInteractor.onTopSiteMenuOpened]
*/
fun handleMenuOpened()
/**
* @see [MessageCardInteractor.onMessageClicked]
*/
@ -248,13 +242,7 @@ class DefaultSessionControlController(
)
}
override fun handleMenuOpened() {
dismissSearchDialogIfDisplayed()
}
override fun handleCollectionOpenTabClicked(tab: ComponentTab) {
dismissSearchDialogIfDisplayed()
restoreUseCase.invoke(
activity,
engine,
@ -307,7 +295,6 @@ class DefaultSessionControlController(
}
override fun handleCollectionShareTabsClicked(collection: TabCollection) {
dismissSearchDialogIfDisplayed()
showShareFragment(
collection.title,
collection.tabs.map { ShareData(url = it.url, title = it.title) },
@ -337,7 +324,6 @@ class DefaultSessionControlController(
}
override fun handlePrivateBrowsingLearnMoreClicked() {
dismissSearchDialogIfDisplayed()
activity.openToBrowserAndLoad(
searchTermOrURL = SupportUtils.getGenericSumoURLForTopic(PRIVATE_BROWSING_MYTHS),
newTab = true,
@ -403,8 +389,6 @@ class DefaultSessionControlController(
}
override fun handleSelectTopSite(topSite: TopSite, position: Int) {
dismissSearchDialogIfDisplayed()
TopSites.openInNewTab.record(NoExtras())
when (topSite) {
@ -501,12 +485,6 @@ class DefaultSessionControlController(
return url
}
private fun dismissSearchDialogIfDisplayed() {
if (navController.currentDestination?.id == R.id.searchDialogFragment) {
navController.navigateUp()
}
}
override fun handleStartBrowsingClicked() {
hideOnboarding()
}
@ -648,14 +626,6 @@ class DefaultSessionControlController(
appStore.dispatch(
AppAction.ModeChange(Mode.fromBrowsingMode(newMode)),
)
if (navController.currentDestination?.id == R.id.searchDialogFragment) {
navController.navigate(
BrowserFragmentDirections.actionGlobalSearchDialog(
sessionId = null,
),
)
}
}
}

@ -133,11 +133,6 @@ interface CollectionInteractor {
* User has removed the collections placeholder from home.
*/
fun onRemoveCollectionsPlaceholder()
/**
* User has opened collection 3 dot menu.
*/
fun onCollectionMenuOpened()
}
interface ToolbarInteractor {
@ -228,11 +223,6 @@ interface TopSiteInteractor {
* "Our sponsors & your privacy" top site menu item.
*/
fun onSponsorPrivacyClicked()
/**
* Called when top site menu is opened.
*/
fun onTopSiteMenuOpened()
}
interface MessageCardInteractor {
@ -367,14 +357,6 @@ class SessionControlInteractor(
controller.handleRemoveCollectionsPlaceholder()
}
override fun onCollectionMenuOpened() {
controller.handleMenuOpened()
}
override fun onTopSiteMenuOpened() {
controller.handleMenuOpened()
}
override fun onRecentTabClicked(tabId: String) {
recentTabController.handleRecentTabClicked(tabId)
}
@ -383,10 +365,6 @@ class SessionControlInteractor(
recentTabController.handleRecentTabShowAllClicked()
}
override fun onRecentTabLongClicked() {
recentTabController.handleRecentTabLongClicked()
}
override fun onRemoveRecentTab(tab: RecentTab.Tab) {
recentTabController.handleRecentTabRemoved(tab)
}
@ -395,10 +373,6 @@ class SessionControlInteractor(
recentSyncedTabController.handleRecentSyncedTabClick(tab)
}
override fun onRecentSyncedTabLongClick() {
recentSyncedTabController.handleRecentSyncedTabLongClick()
}
override fun onSyncedTabShowAllClicked() {
recentSyncedTabController.handleSyncedTabShowAllClicked()
}
@ -419,10 +393,6 @@ class SessionControlInteractor(
recentBookmarksController.handleBookmarkRemoved(bookmark)
}
override fun onRecentBookmarkLongClicked() {
recentBookmarksController.handleBookmarkLongClicked()
}
override fun onHistoryShowAllClicked() {
recentVisitsController.handleHistoryShowAllClicked()
}
@ -445,10 +415,6 @@ class SessionControlInteractor(
recentVisitsController.handleRemoveRecentHistoryHighlight(highlightUrl)
}
override fun onRecentVisitLongClicked() {
recentVisitsController.handleRecentVisitLongClicked()
}
override fun openCustomizeHomePage() {
controller.handleCustomizeHomeTapped()
}

@ -54,7 +54,6 @@ class TopSiteItemViewHolder(
init {
itemView.setOnLongClickListener {
interactor.onTopSiteMenuOpened()
TopSites.longPress.record(TopSites.LongPressExtra(topSite.name()))
val topSiteMenu = TopSiteItemMenu(

@ -81,6 +81,8 @@ class ToolbarView(
false
}
setDefaultIcon()
setOnEditListener(
object : mozilla.components.concept.toolbar.Toolbar.OnEditListener {
override fun onCancelEditing(): Boolean {
@ -110,7 +112,9 @@ class ToolbarView(
view.editMode()
isInitialized = true
}
}
private fun setDefaultIcon() {
val bookmarkSearchIcon =
AppCompatResources.getDrawable(context, R.drawable.ic_bookmarks_menu)

@ -81,6 +81,8 @@ class ToolbarView(
false
}
setDefaultIcon()
setOnEditListener(
object : mozilla.components.concept.toolbar.Toolbar.OnEditListener {
override fun onCancelEditing(): Boolean {
@ -110,7 +112,9 @@ class ToolbarView(
view.editMode()
isInitialized = true
}
}
private fun setDefaultIcon() {
val historySearchIcon = AppCompatResources.getDrawable(context, R.drawable.ic_history)
historySearchIcon?.let {

@ -0,0 +1,64 @@
/* 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.onboarding
import android.annotation.SuppressLint
import android.content.pm.ActivityInfo
import android.os.Bundle
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 com.google.accompanist.insets.ProvideWindowInsets
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.onboarding.view.NotificationPermissionDialogScreen
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Dialog displaying notification pre-permission prompt.
*/
class HomeNotificationPermissionDialogFragment : DialogFragment() {
@SuppressLint("SourceLockedOrientationActivity")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, R.style.HomeOnboardingDialogStyle)
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
override fun onDestroy() {
super.onDestroy()
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View = ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
ProvideWindowInsets {
FirefoxTheme {
NotificationPermissionDialogScreen(
onDismiss = ::onDismiss,
grantNotificationPermission = {
ensureMarketingChannelExists(context.applicationContext)
onDismiss()
},
)
}
}
}
}
private fun onDismiss() {
dismiss()
context?.settings()?.isNotificationPrePermissionShown = true
}
}

@ -8,13 +8,11 @@ import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import androidx.core.app.NotificationManagerCompat
import org.mozilla.fenix.GleanMetrics.Events.marketingNotificationAllowed
import org.mozilla.fenix.R
// Channel ID was not updated when it was renamed to marketing. Thus, we'll have to continue
// to use this ID as the marketing channel ID
private const val MARKETING_CHANNEL_ID = "org.mozilla.fenix.default.browser.channel"
const val MARKETING_CHANNEL_ID = "org.mozilla.fenix.default.browser.channel"
// For notification that uses the marketing notification channel, IDs should be unique.
const val DEFAULT_BROWSER_NOTIFICATION_ID = 1
@ -26,7 +24,6 @@ const val RE_ENGAGEMENT_NOTIFICATION_ID = 2
* Returns the channel id to be used for notifications.
*/
fun ensureMarketingChannelExists(context: Context): String {
var channelEnabled = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@ -43,18 +40,7 @@ fun ensureMarketingChannelExists(context: Context): String {
notificationManager.createNotificationChannel(channel)
}
channelEnabled = channel.importance != NotificationManager.IMPORTANCE_NONE
}
@Suppress("TooGenericExceptionCaught")
val notificationsEnabled = try {
NotificationManagerCompat.from(context).areNotificationsEnabled()
} catch (e: Exception) {
false
}
marketingNotificationAllowed.set(notificationsEnabled && channelEnabled)
return MARKETING_CHANNEL_ID
}

@ -0,0 +1,233 @@
/* 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.onboarding.view
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.accompanist.insets.navigationBarsPadding
import com.google.accompanist.insets.statusBarsPadding
import mozilla.components.service.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.Onboarding
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.button.PrimaryButton
import org.mozilla.fenix.compose.button.SecondaryButton
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Model containing data for the [NotificationPermissionPage].
*
* @param image [DrawableRes] displayed on the page.
* @param title [StringRes] of the permission headline text.
* @param description [StringRes] of the permission body text.
* @param primaryButtonText [StringRes] of the primary button text.
* @param secondaryButtonText [StringRes] of the secondary button text.
* @param onRecordImpressionEvent Callback for recording impression event.
*/
private data class NotificationPermissionPageState(
@DrawableRes val image: Int,
@StringRes val title: Int,
@StringRes val description: Int,
@StringRes val primaryButtonText: Int,
@StringRes val secondaryButtonText: Int? = null,
val onRecordImpressionEvent: () -> Unit,
)
/**
* A screen for displaying notification pre permission prompt.
*
* @param onDismiss Invoked when the user clicks on the close or the negative button.
* @param grantNotificationPermission Invoked when the user clicks on the positive button.
*/
@Composable
fun NotificationPermissionDialogScreen(
onDismiss: () -> Unit,
grantNotificationPermission: () -> Unit,
) {
NotificationPermissionContent(
notificationPermissionPageState = NotificationPageState,
onDismiss = {
onDismiss()
Onboarding.notifPppCloseClick.record(NoExtras())
},
onPrimaryButtonClick = {
grantNotificationPermission()
Onboarding.notifPppPositiveBtnClick.record(NoExtras())
},
onSecondaryButtonClick = {
onDismiss()
Onboarding.notifPppNegativeBtnClick.record(NoExtras())
},
)
}
@Composable
private fun NotificationPermissionContent(
notificationPermissionPageState: NotificationPermissionPageState,
onDismiss: () -> Unit,
onPrimaryButtonClick: () -> Unit,
onSecondaryButtonClick: () -> Unit,
modifier: Modifier = Modifier,
) {
BoxWithConstraints(Modifier.fillMaxSize()) {
val boxWithConstraintsScope = this
Column(
modifier = modifier
.background(FirefoxTheme.colors.layer1)
.fillMaxSize()
.padding(bottom = 32.dp)
.statusBarsPadding()
.navigationBarsPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
IconButton(
onClick = onDismiss,
modifier = Modifier.align(Alignment.End),
) {
Icon(
painter = painterResource(id = R.drawable.mozac_ic_close),
contentDescription = stringResource(R.string.content_description_close_button),
tint = FirefoxTheme.colors.iconPrimary,
)
}
NotificationPermissionPage(
pageState = notificationPermissionPageState,
onPrimaryButtonClick = onPrimaryButtonClick,
onSecondaryButtonClick = onSecondaryButtonClick,
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState()),
imageModifier = Modifier
.height(boxWithConstraintsScope.maxHeight.times(IMAGE_HEIGHT_RATIO)),
)
}
}
}
/**
* A page for displaying Notification Permission Content.
*
* @param pageState The page content that's displayed.
* @param onPrimaryButtonClick Invoked when the user clicks the primary button.
* @param onSecondaryButtonClick Invoked when the user clicks the secondary button.
* @param modifier The modifier to be applied to the Composable.
*/
@Composable
private fun NotificationPermissionPage(
pageState: NotificationPermissionPageState,
onPrimaryButtonClick: () -> Unit,
onSecondaryButtonClick: () -> Unit,
modifier: Modifier = Modifier,
imageModifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceEvenly,
) {
Image(
painter = painterResource(id = pageState.image),
contentDescription = null,
modifier = imageModifier,
)
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResource(
id = pageState.title,
formatArgs = arrayOf(stringResource(R.string.app_name)),
),
color = FirefoxTheme.colors.textPrimary,
textAlign = TextAlign.Center,
style = FirefoxTheme.typography.headline5,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(
id = pageState.description,
formatArgs = arrayOf(stringResource(R.string.app_name)),
),
color = FirefoxTheme.colors.textSecondary,
textAlign = TextAlign.Center,
style = FirefoxTheme.typography.body2,
)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(top = 16.dp),
) {
PrimaryButton(
text = stringResource(id = pageState.primaryButtonText),
onClick = onPrimaryButtonClick,
)
if (pageState.secondaryButtonText != null) {
Spacer(modifier = Modifier.height(8.dp))
SecondaryButton(
text = stringResource(id = pageState.secondaryButtonText),
onClick = onSecondaryButtonClick,
)
}
}
}
LaunchedEffect(pageState) {
pageState.onRecordImpressionEvent()
}
}
private val NotificationPageState = NotificationPermissionPageState(
image = R.drawable.ic_notification_permission,
title = R.string.onboarding_home_enable_notifications_title,
description = R.string.onboarding_home_enable_notifications_description,
primaryButtonText = R.string.onboarding_home_enable_notifications_positive_button,
secondaryButtonText = R.string.onboarding_home_enable_notifications_negative_button,
onRecordImpressionEvent = { Onboarding.notifPppImpression.record(NoExtras()) },
)
private const val IMAGE_HEIGHT_RATIO = 0.4f
@Preview
@Composable
private fun NotificationPermissionScreenPreview() {
FirefoxTheme {
NotificationPermissionDialogScreen(
grantNotificationPermission = {},
onDismiss = { },
)
}
}

@ -164,7 +164,7 @@ private fun OnboardingWelcomeBottomContent(
@Composable
private fun OnboardingWelcomeContent() {
Column(
modifier = Modifier.padding(horizontal = 16.dp),
modifier = Modifier.padding(horizontal = 16.dp, vertical = 32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
@ -195,7 +195,7 @@ private fun OnboardingWelcomeContent() {
@Composable
private fun OnboardingSyncSignInContent() {
Column(
modifier = Modifier.padding(horizontal = 16.dp),
modifier = Modifier.padding(horizontal = 16.dp, vertical = 32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(

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

Loading…
Cancel
Save