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

pull/600/head
akliuxingyuan 1 year ago
commit e95c430d11

@ -3,6 +3,7 @@ projects:
upstream_dependencies:
- browser-domains
- browser-engine-gecko
- browser-errorpages
- browser-icons
- browser-menu
- browser-menu2
@ -17,6 +18,7 @@ projects:
- concept-awesomebar
- concept-base
- concept-engine
- concept-fetch
- concept-menu
- concept-push
- concept-storage
@ -78,10 +80,12 @@ projects:
- support-rusterrors
- support-rusthttp
- support-rustlog
- support-sync-telemetry
- support-test
- support-test-libstate
- support-utils
- support-webextensions
- ui-autocomplete
- ui-colors
- ui-icons
- ui-tabcounter

@ -1,43 +0,0 @@
# Definitions for jobs that run periodically. For details on the format, see
# `taskcluster/taskgraph/cron/schema.py`. For documentation, see
# `taskcluster/docs/cron.rst`.
---
jobs:
- name: nightly
job:
type: decision-task
treeherder-symbol: Nd
target-tasks-method: nightly
when:
- {hour: 5, minute: 0}
- {hour: 17, minute: 0}
- name: nightly-test
job:
type: decision-task
treeherder-symbol: Nt
target-tasks-method: nightly-test
when:
- {hour: 5, minute: 0}
- name: fennec-production
job:
type: decision-task
treeherder-symbol: fennec-production
target-tasks-method: fennec-production
when: [] # Force hook only
- name: screenshots
job:
type: decision-task
treeherder-symbol: screenshots-D
target-tasks-method: screenshots
when: [{weekday: 'Monday', hour: 10, minute: 0}]
- name: legacy-api-ui-tests
job:
type: decision-task
treeherder-symbol: legacy-api-ui
target-tasks-method: legacy_api_ui_tests
when:
- {hour: 9, minute: 0}
- {hour: 12, minute: 0}
- {hour: 18, minute: 0}
- {hour: 22, minute: 0}

31
.github/CODEOWNERS vendored

@ -1,31 +0,0 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# This CODEOWNERS file defines individuals or teams that are responsible
# for code in this repository. Code owners are automatically requested
# for review when someone opens a pull request that modifies code that
# they own. Order is important; the last matching pattern takes the most
# precedence.
# A CODEOWNERS file uses a pattern that follows the same rules used in
# gitignore files. The pattern is followed by one or more GitHub usernames
# or team names using the standard @username or @org/team-name format.
# You can also refer to a user by an email address that has been added
# to their GitHub account, for example user@example.com.
# https://help.github.com/articles/about-codeowners/
# WARNING: if there is a single syntax error in this file, CODEOWNERS
# WILL NOT WORK AT ALL. Please be careful when editing this file.
#
# You can use the technique described in this blog post to validate
# the paths you specify in .gitignore:
# http://www.benjaminoakes.com/git/2018/08/10/Testing-changes-to-GitHub-CODEOWNERS/
# By default the Android Components team will be the owner for everything in
# the repo. Unless a later match takes precedence.
* @mozilla-mobile/ACT @mozilla-mobile/fenix
/.cron.yml @mozilla-mobile/releng @mozilla-mobile/fenix
/.taskcluster.yml @mozilla-mobile/releng @mozilla-mobile/fenix
/automation/ @mozilla-mobile/fenix
/taskcluster/ @mozilla-mobile/releng @mozilla-mobile/fenix
/.github/ @mozilla-mobile/fenix

@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: We are now using Bugzilla for issue tracking!
url: https://bugzilla.mozilla.org/enter_bug.cgi?product=Fenix
about: Please file your ticket there, and select the corresponding component from the list. If the component is not in the list, select "General". In commit messages, please reference the ticket using "Bug 1234567 - " as prefix.

@ -1,23 +0,0 @@
### Pull Request checklist
<!-- Before submitting the PR, please address each item -->
- [ ] **Tests**: This PR includes thorough tests or an explanation of why it does not
- [ ] **Screenshots**: This PR includes screenshots or GIFs of the changes made or an explanation of why it does not
- [ ] **Accessibility**: The code in this PR follows [accessibility best practices](https://github.com/mozilla-mobile/shared-docs/blob/master/android/accessibility_guide.md) or does not include any user facing features. In addition, it includes a screenshot of a successful [accessibility scan](https://play.google.com/store/apps/details?id=com.google.android.apps.accessibility.auditor&hl=en_US) to ensure no new defects are added to the product.
### QA
<!-- Before submitting the PR, please address each item -->
- [x] **QA Needed**
### To download an APK when reviewing a PR (after all CI tasks finished running):
1. Click on `Checks` at the top of the PR page.
2. Click on the `firefoxci-taskcluster` group on the left to expand all tasks.
3. Click on the `build-debug` task.
4. Click on `View task in Taskcluster` in the new `DETAILS` section.
5. The APK links should be on the right side of the screen, named for each CPU architecture.
### GitHub Automation
<!-- Do not add anything below this line -->
Used by GitHub Actions.

63
.github/stale.yml vendored

@ -1,63 +0,0 @@
# Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 180
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 7
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
onlyLabels: []
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- pin
- "feature request 🌟"
- "eng:disabled-test"
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: false
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: false
# Set to true to ignore issues with an assignee (defaults to false)
exemptAssignees: false
# Label to use when marking as stale
staleLabel: wontfix
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
See: https://github.com/mozilla-mobile/fenix/issues/17373
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when removing the stale label.
# unmarkComment: >
# Your comment here.
# Comment to post when closing a stale Issue or Pull Request.
# closeComment: >
# Your comment here.
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 30
# Limit to only `issues` or `pulls`
only: issues
# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
# pulls:
# daysUntilStale: 30
# markComment: >
# This pull request has been automatically marked as stale because it has not had
# recent activity. It will be closed if no further activity occurs. Thank you
# for your contributions.
issues:
exemptLabels:
- pin
- "feature request 🌟"

@ -1,16 +0,0 @@
name: AssignTriageLabel
on:
issues:
types: [opened]
jobs:
assign:
name: Triage Issues
runs-on: ubuntu-latest
steps:
- name: Add Triage Label
uses: boek/AddTriageLabel@v1.2
with:
repotoken: ${{ secrets.GITHUB_TOKEN }}
labeltoadd: "needs:triage"

@ -107,13 +107,16 @@ pre-permission-notification-prompt:
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.
description: A feature that shows the re-engagement notification if the user is inactive.
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "If true, the re-engagement notification is shown to the inactive user."
type:
type: int
description: The type of re-engagement notification that is shown to the inactive user.
search-term-groups:
description: A feature allowing the grouping of URLs around the search term that it came from.
hasExposure: true

@ -33,6 +33,11 @@ android {
testBuildType project.property("testBuildType")
}
// This allows overriding the target activity for MozillaOnline builds, which happens
// as part of the defaultConfig below, and applies to all other configurations (Nightly,
// Beta, and Release.)
def targetActivity = "HomeActivity"
defaultConfig {
applicationId "io.github.forkmaintainers"
minSdkVersion Config.minSdkVersion
@ -66,16 +71,18 @@ android {
buildConfigField "String", "AMO_SERVER_URL", "\"https://services.addons.mozilla.org\""
def deepLinkSchemeValue = "fenix-dev"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = [
"deepLinkScheme": deepLinkSchemeValue
]
// Build flag for "Mozilla Online" variants. See `Config.isMozillaOnline`.
if (project.hasProperty("mozillaOnline") || gradle.hasProperty("localProperties.mozillaOnline")) {
buildConfigField "boolean", "MOZILLA_ONLINE", "true"
targetActivity = "MozillaOnlineHomeActivity"
} else {
buildConfigField "boolean", "MOZILLA_ONLINE", "false"
}
manifestPlaceholders = [
"targetActivity": targetActivity,
"deepLinkScheme": deepLinkSchemeValue
]
}
def releaseTemplate = {
@ -112,7 +119,10 @@ android {
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
def deepLinkSchemeValue = "fenix-nightly"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = ["deepLinkScheme": deepLinkSchemeValue]
manifestPlaceholders = [
"deepLinkScheme": deepLinkSchemeValue,
"targetActivity": targetActivity
]
}
beta releaseTemplate >> {
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
@ -128,7 +138,8 @@ android {
// - https://issuetracker.google.com/issues/36924841
// - https://issuetracker.google.com/issues/36905922
"sharedUserId": "org.mozilla.firefox.sharedID",
"deepLinkScheme": deepLinkSchemeValue
"deepLinkScheme": deepLinkSchemeValue,
"targetActivity": targetActivity
]
}
release releaseTemplate >> {
@ -145,7 +156,8 @@ android {
// - https://issuetracker.google.com/issues/36924841
// - https://issuetracker.google.com/issues/36905922
"sharedUserId": "org.mozilla.firefox.sharedID",
"deepLinkScheme": deepLinkSchemeValue
"deepLinkScheme": deepLinkSchemeValue,
"targetActivity": targetActivity,
]
}
forkDebug {
@ -182,7 +194,7 @@ android {
viewBinding true
}
aaptOptions {
androidResources {
// All JavaScript code used internally by GeckoView is packaged in a
// file called omni.ja. If this file is compressed in the APK,
// GeckoView must uncompress it before it can do anything else which
@ -227,21 +239,21 @@ android {
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
lintOptions {
lint {
lintConfig file("lint.xml")
baseline file("lint-baseline.xml")
}
packagingOptions {
exclude 'META-INF/atomicfu.kotlin_module'
exclude 'META-INF/AL2.0'
exclude 'META-INF/LGPL2.1'
resources {
excludes += ['META-INF/atomicfu.kotlin_module', 'META-INF/AL2.0', 'META-INF/LGPL2.1']
}
}
testOptions {
unitTests.returnDefaultValues = true
@ -264,6 +276,7 @@ android {
kotlinCompilerExtensionVersion = FenixVersions.androidx_compose_compiler
}
namespace 'org.mozilla.fenix'
}
android.applicationVariants.all { variant ->
@ -277,7 +290,7 @@ android.applicationVariants.all { variant ->
println("----------------------------------------------")
println("Variant name: " + variant.name)
println("Application ID: " + [variant.mergedFlavor.applicationId, variant.buildType.applicationIdSuffix].findAll().join())
println("Application ID: " + [variant.applicationId, variant.buildType.applicationIdSuffix].findAll().join())
println("Build type: " + variant.buildType.name)
println("Flavor: " + variant.flavorName)
println("Telemetry enabled: " + !isDebug)
@ -460,14 +473,12 @@ android.applicationVariants.all { variant ->
// Generate Kotlin code for the Fenix Glean metrics.
apply plugin: "org.mozilla.telemetry.glean-gradle-plugin"
apply plugin: "org.mozilla.components.nimbus-gradle-plugin"
apply plugin: "org.mozilla.appservices.nimbus-gradle-plugin"
nimbus {
// The path to the Nimbus feature manifest file
manifestFile = "nimbus.fml.yaml"
// The fully qualified class name for the generated features.
// If the classname begins with a '.' this is taken as a suffix to the app's package name
destinationClass = ".nimbus.FxNimbus"
// Map from the variant name to the channel as experimenter and nimbus understand it.
// If nimbus's channels were accurately set up well for this project, then this
// shouldn't be needed.
@ -483,6 +494,7 @@ nimbus {
// and used to inform experiment configuration.
experimenterManifest = ".experimenter.yaml"
}
configurations {
// There's an interaction between Gradle's resolution of dependencies with different types
// (@jar, @aar) for `implementation` and `testImplementation` and with Android Studio's built-in
@ -512,7 +524,6 @@ dependencies {
implementation project(':browser-engine-gecko')
implementation FenixDependencies.kotlin_stdlib
implementation FenixDependencies.kotlin_coroutines
implementation FenixDependencies.kotlin_coroutines_android
testImplementation FenixDependencies.kotlin_coroutines_test
@ -520,7 +531,7 @@ dependencies {
implementation FenixDependencies.androidx_constraintlayout
implementation FenixDependencies.androidx_coordinatorlayout
implementation FenixDependencies.google_accompanist_drawablepainter
implementation FenixDependencies.google_accompanist_insets
implementation FenixDependencies.google_accompanist_pager
implementation FenixDependencies.sentry
@ -674,6 +685,7 @@ dependencies {
androidTestImplementation FenixDependencies.androidx_junit
androidTestImplementation FenixDependencies.androidx_test_extensions
androidTestImplementation FenixDependencies.androidx_tracing
androidTestImplementation FenixDependencies.androidx_work_testing
androidTestImplementation FenixDependencies.androidx_benchmark_junit4
androidTestImplementation FenixDependencies.mockwebserver
@ -870,20 +882,6 @@ if (gradle.hasProperty('localProperties.dependencySubstitutions.geckoviewTopsrcd
apply from: "${topsrcdir}/substitute-local-geckoview.gradle"
}
def acSrcDir = null
if (gradle.hasProperty('localProperties.autoPublish.android-components.dir')) {
acSrcDir = gradle.getProperty('localProperties.autoPublish.android-components.dir')
} else if (gradle.hasProperty('localProperties.branchBuild.android-components.dir')) {
acSrcDir = gradle.getProperty('localProperties.branchBuild.android-components.dir')
}
if (acSrcDir) {
if (acSrcDir.startsWith("/")) {
apply from: "${acSrcDir}/substitute-local-ac.gradle"
} else {
apply from: "../${acSrcDir}/substitute-local-ac.gradle"
}
}
def appServicesSrcDir = null
if (gradle.hasProperty('localProperties.autoPublish.application-services.dir')) {
appServicesSrcDir = gradle.getProperty('localProperties.autoPublish.application-services.dir')

@ -70,6 +70,11 @@ features:
DEVICE_IOS: os == 'iOS'
ALWAYS: "true"
NEVER: "false"
DAY_1_AFTER_INSTALL: days_since_install == 1
DAY_2_AFTER_INSTALL: days_since_install == 2
DAY_3_AFTER_INSTALL: days_since_install == 3
DAY_4_AFTER_INSTALL: days_since_install == 4
DAY_5_AFTER_INSTALL: days_since_install == 5
# Using custom attributes for the browser
I_AM_DEFAULT_BROWSER: "is_default_browser"
@ -144,6 +149,15 @@ features:
trigger: [ "I_AM_NOT_DEFAULT_BROWSER","USER_ESTABLISHED_INSTALL" ]
style: "PERSISTENT"
button-label: preferences_set_as_default_browser
default-browser-notification:
title: nimbus_notification_default_browser_title
text: nimbus_notification_default_browser_text
surface: notification
style: NOTIFICATION
trigger:
- I_AM_NOT_DEFAULT_BROWSER
- DAY_3_AFTER_INSTALL
action: MAKE_DEFAULT_BROWSER
- channel: developer
value:
@ -156,16 +170,6 @@ features:
max-display-count: 1
notification-config:
refresh-interval: 120 # minutes (2 hours)
messages:
default-browser-notification:
title: preferences_set_as_default_browser
text: default_browser_experiment_card_text
surface: notification
style: NOTIFICATION
action: MAKE_DEFAULT_BROWSER
trigger:
- I_AM_NOT_DEFAULT_BROWSER
- INACTIVE_2_DAYS
objects:
MessageData:

@ -184,33 +184,6 @@ events:
metadata:
tags:
- Toolbar
default_browser_notif_tapped:
type: event
description: |
User tapped on the default browser notification
bugs:
- https://github.com/mozilla-mobile/fenix/issues/19847
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/20311
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
- kbrosnan@mozilla.com
expires: never
default_browser_notif_shown:
type: event
description: |
Default browser notification was shown to the user
bugs:
- https://github.com/mozilla-mobile/fenix/issues/27779
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/27780
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 122
re_engagement_notif_tapped:
type: event
description: |
@ -1916,11 +1889,12 @@ metrics:
- https://github.com/mozilla-mobile/fenix/issues/25538
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/25539#issuecomment-1163393066
- https://github.com/mozilla-mobile/firefox-android/pull/1101
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 114
expires: 126
metadata:
tags:
- Wallpapers
@ -2505,20 +2479,17 @@ preferences:
tags:
- Settings
open_links_in_app_enabled:
type: boolean
type: string
description: |
Whether or not the user has the open links in apps feature enabled.
default: false
The user has the open links in apps feature enabled.
"ask_before_opening", "always" or "never".
default: "never"
send_in_pings:
- metrics
bugs:
- https://github.com/mozilla-mobile/fenix/issues/11118
- https://bugzilla.mozilla.org/show_bug.cgi?id=1818085
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/11446
- https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068
- https://github.com/mozilla-mobile/fenix/pull/19924#issuecomment-861423789
- https://github.com/mozilla-mobile/fenix/pull/20517#pullrequestreview-718069041
- https://github.com/mozilla-mobile/fenix/pull/23453#issuecomment-1024694220
- https://github.com/mozilla-mobile/firefox-android/pull/1054
data_sensitivity:
- interaction
notification_emails:
@ -4597,11 +4568,13 @@ tabs_tray:
- https://github.com/mozilla-mobile/fenix/issues/24549
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/24671
- https://github.com/mozilla-mobile/firefox-android/pull/1101
notification_emails:
- android-probes@mozilla.com
- kbrosnan@mozilla.com
data_sensitivity:
- interaction
expires: 114
expires: never
collections:
@ -4923,6 +4896,23 @@ collections:
metadata:
tags:
- Collections
placeholder_cancel:
type: event
description: |
User interacted with the `X` button from the homescreen no collections
placeholder.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1821032
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1167
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 122
metadata:
tags:
- Collections
search_widget:
new_tab_button:
@ -6155,6 +6145,10 @@ pocket:
type: string
description: |
Id of the shown sponsored story.
Until version 112 this will be the `flight_id` allowing to
identify stories across different campaigns.
From version 112 this will be the story `id` identifying a
specific story irrespective of the campaign it is part of.
times_shown:
type: string
description: |
@ -6211,6 +6205,10 @@ pocket:
type: string
description: |
Id of the shown sponsored story.
Until version 112 this will be the `flight_id` allowing to
identify stories across different campaigns.
From version 112 this will be the story `id` identifying a
specific story irrespective of the campaign it is part of.
times_shown:
type: string
description: |
@ -7434,6 +7432,23 @@ progressive_web_app:
metadata:
tags:
- PWA
onboarding_cancel:
type: event
description: |
User interacts with the onboarding PWA dialog cancel button which takes
them to the website.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1821030
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1164
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 122
metadata:
tags:
- PWA
tabs:
setting_opened:
@ -8063,11 +8078,13 @@ start_on_home:
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/19885
- https://github.com/mozilla-mobile/fenix/pull/24982
- https://github.com/mozilla-mobile/firefox-android/pull/1101
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 114
- kbrosnan@mozilla.com
expires: never
metadata:
tags:
- HomeScreen
@ -8080,11 +8097,13 @@ start_on_home:
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/19885
- https://github.com/mozilla-mobile/fenix/pull/24982
- https://github.com/mozilla-mobile/firefox-android/pull/1101
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 114
- kbrosnan@mozilla.com
expires: never
metadata:
tags:
- HomeScreen
@ -8099,11 +8118,12 @@ recent_tabs:
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/20138
- https://github.com/mozilla-mobile/fenix/pull/24982
- https://github.com/mozilla-mobile/firefox-android/pull/1101
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 114
expires: 126
metadata:
tags:
- RecentTabs
@ -8116,28 +8136,13 @@ recent_tabs:
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/20138
- https://github.com/mozilla-mobile/fenix/pull/24982
- https://github.com/mozilla-mobile/firefox-android/pull/1101
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 114
metadata:
tags:
- RecentTabs
in_progress_media_tab_opened:
type: event
description: |
User has opened a recent media tab from homescreen.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/20393
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/20138
- https://github.com/mozilla-mobile/fenix/pull/24982
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 114
- kbrosnan@mozilla.com
expires: never
metadata:
tags:
- RecentTabs
@ -8700,12 +8705,14 @@ messaging:
bugs:
- https://github.com/mozilla-mobile/fenix/issues/24224
data_reviews:
- https://github.com/mozilla-mobile/fenix/issues/24224
- https://github.com/mozilla-mobile/fenix/pull/24426
- https://github.com/mozilla-mobile/firefox-android/pull/1101
notification_emails:
- android-probes@mozilla.com
- kbrosnan@mozilla.com
data_sensitivity:
- interaction
expires: 114
expires: never
message_dismissed:
type: event
description: |
@ -8718,11 +8725,13 @@ messaging:
- https://github.com/mozilla-mobile/fenix/issues/24224
data_reviews:
- https://github.com/mozilla-mobile/fenix/issues/24224
- https://github.com/mozilla-mobile/firefox-android/pull/1101
notification_emails:
- android-probes@mozilla.com
- kbrosnan@mozilla.com
data_sensitivity:
- interaction
expires: 114
expires: never
message_clicked:
type: event
description: |
@ -8738,11 +8747,13 @@ messaging:
- https://github.com/mozilla-mobile/fenix/issues/24224
data_reviews:
- https://github.com/mozilla-mobile/fenix/issues/24224
- https://github.com/mozilla-mobile/firefox-android/pull/1101
notification_emails:
- android-probes@mozilla.com
- kbrosnan@mozilla.com
data_sensitivity:
- interaction
expires: 114
expires: never
message_expired:
type: event
description: |
@ -8755,11 +8766,13 @@ messaging:
- https://github.com/mozilla-mobile/fenix/issues/24224
data_reviews:
- https://github.com/mozilla-mobile/fenix/issues/24224
- https://github.com/mozilla-mobile/firefox-android/pull/1101
notification_emails:
- android-probes@mozilla.com
- kbrosnan@mozilla.com
data_sensitivity:
- interaction
expires: 114
expires: never
malformed:
type: event
description: |
@ -8772,11 +8785,13 @@ messaging:
- https://github.com/mozilla-mobile/fenix/issues/24224
data_reviews:
- https://github.com/mozilla-mobile/fenix/issues/24224
- https://github.com/mozilla-mobile/firefox-android/pull/1101
notification_emails:
- android-probes@mozilla.com
- kbrosnan@mozilla.com
data_sensitivity:
- interaction
expires: 114
expires: never
wallpapers:
wallpaper_settings_opened:
@ -8787,11 +8802,12 @@ wallpapers:
- https://github.com/mozilla-mobile/fenix/issues/23381
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/23382
- https://github.com/mozilla-mobile/firefox-android/pull/1101
notification_emails:
- android-probes@mozilla.com
data_sensitivity:
- interaction
expires: 114
expires: 126
no_lint:
- COMMON_PREFIX
metadata:
@ -8913,11 +8929,12 @@ recently_visited_homepage:
- https://github.com/mozilla-mobile/fenix/issues/23821
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/23909
- https://github.com/mozilla-mobile/firefox-android/pull/1101
notification_emails:
- android-probes@mozilla.com
data_sensitivity:
- interaction
expires: 114
expires: 126
search_group_opened:
type: event
description: |
@ -8926,11 +8943,12 @@ recently_visited_homepage:
- https://github.com/mozilla-mobile/fenix/issues/23821
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/23909
- https://github.com/mozilla-mobile/firefox-android/pull/1101
notification_emails:
- android-probes@mozilla.com
data_sensitivity:
- interaction
expires: 114
expires: 126
recent_synced_tabs:
recent_synced_tab_shown:
@ -8942,11 +8960,12 @@ recent_synced_tabs:
- https://github.com/mozilla-mobile/fenix/issues/24549
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/24671
- https://github.com/mozilla-mobile/firefox-android/pull/1101
notification_emails:
- android-probes@mozilla.com
data_sensitivity:
- interaction
expires: 114
expires: 126
recent_synced_tab_time_to_load:
type: timing_distribution
time_unit: millisecond
@ -8956,11 +8975,12 @@ recent_synced_tabs:
- https://github.com/mozilla-mobile/fenix/issues/24549
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/24671
- https://github.com/mozilla-mobile/firefox-android/pull/1101
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 114
expires: 126
recent_synced_tab_opened:
type: labeled_counter
description: |
@ -8970,11 +8990,12 @@ recent_synced_tabs:
- https://github.com/mozilla-mobile/fenix/issues/24549
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/24671
- https://github.com/mozilla-mobile/firefox-android/pull/1101
notification_emails:
- android-probes@mozilla.com
data_sensitivity:
- interaction
expires: 114
expires: 126
show_all_synced_tabs_clicked:
type: counter
description: |
@ -8983,11 +9004,12 @@ recent_synced_tabs:
- https://github.com/mozilla-mobile/fenix/issues/24549
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/24671
- https://github.com/mozilla-mobile/firefox-android/pull/1101
notification_emails:
- android-probes@mozilla.com
data_sensitivity:
- interaction
expires: 114
expires: 126
latest_synced_tab_is_stale:
type: counter
description: |
@ -8997,11 +9019,12 @@ recent_synced_tabs:
- https://github.com/mozilla-mobile/fenix/issues/24549
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/24671
- https://github.com/mozilla-mobile/firefox-android/pull/1101
notification_emails:
- android-probes@mozilla.com
data_sensitivity:
- interaction
expires: 114
expires: 126
review_prompt:
prompt_attempt:
type: event
@ -9080,3 +9103,29 @@ client_deduplication:
- android-probes@mozilla.com
- fbertsch@mozilla.com
expires: 122
private_browsing_shortcut_cfr:
add_shortcut:
type: event
description: |
Pivate browsing CFR "Add shortcut" button pressed.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/4658
- https://bugzilla.mozilla.org/show_bug.cgi?id=1819909
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1139
notification_emails:
- android-probes@mozilla.com
expires: 122
cancel:
type: event
description: |
Pivate browsing CFR "No thanks" button pressed.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/4658
- https://bugzilla.mozilla.org/show_bug.cgi?id=1819909
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1139
notification_emails:
- android-probes@mozilla.com
expires: 122

@ -11,7 +11,7 @@ channels:
- developer
- forkRelease
includes:
- ./messaging.fml.yaml
- messaging.fml.yaml
features:
homescreen:
description: The homescreen that the user goes to when they press home or new tab.
@ -82,8 +82,8 @@ features:
"sync-cfr": true,
"wallpapers-selection-tool": true,
"jump-back-in-cfr": true,
"tcp-cfr": false,
"tcp-feature": false,
"tcp-cfr": true,
"tcp-feature": true,
}
defaults:
- channel: developer
@ -106,8 +106,7 @@ features:
{
"feature-ui": 0,
"feature-setting-value": 0,
"dialog-re-engage-time": 4,
"dialog-text-variant": 0,
"dialog-re-engage-time": 4
}
defaults:
- channel: developer
@ -115,8 +114,7 @@ features:
"sections-enabled": {
"feature-ui": 1,
"feature-setting-value": 0,
"dialog-re-engage-time": 4,
"dialog-text-variant": 0,
"dialog-re-engage-time": 4
}
}
- channel: nightly
@ -124,8 +122,7 @@ features:
"sections-enabled": {
"feature-ui": 1,
"feature-setting-value": 0,
"dialog-re-engage-time": 4,
"dialog-text-variant": 0,
"dialog-re-engage-time": 4
}
}
unified-search:
@ -155,6 +152,21 @@ features:
value:
enabled: false
client-deduplication:
description: A feature to control the sending of the client-deduplication ping.
variables:
enabled:
description: If true, the ping will be sent.
type: Boolean
default: false
defaults:
- channel: nightly
value:
enabled: false
- channel: developer
value:
enabled: false
growth-data:
description: A feature measuring campaign growth data
variables:
@ -168,12 +180,16 @@ features:
enabled: true
re-engagement-notification:
description: A feature that shows the re-enagement notification if the user is inactive.
description: A feature that shows the re-engagement notification if the user is inactive.
variables:
enabled:
description: If true, the re-engagement notification is shown to the inactive user.
type: Boolean
default: false
type:
description: The type of re-engagement notification that is shown to the inactive user.
type: Int
default: 0
pre-permission-notification-prompt:
description: A feature that shows the pre-permission notification prompt.
@ -241,10 +257,6 @@ types:
description: An integer indicating the number of hours that needs to happen before
the re-engagement dialog shows again since the last seen, for example if set to 4
that means if the users has seen the dialog, it will see it 4 hours later.
dialog-text-variant:
description: An integer from 0 to 2, indicating which text variant should be used on the re-engagement dialog.
0 to indicate the variant control should be used, 1 indicates the variant 1 should be used,
2 indicates the variant 2 should be used, each variant can be found in the mockups for cookie banner handling.
OnboardingPanel:
description: The types of onboarding panels in the onboarding page
variants:

@ -5,9 +5,13 @@
</head>
<body>
<form>
<p>Card information</p>
<p>Card Number: <input id="cardNumber" type="text" placeholder="1234 1234 1234 1234"></p>
<p>Name on card: <input id="nameOnCard"type="text" placeholder="Name on card"></p>
<p>Card information</p>
<p>Card Number: <input id="cardNumber" type="text" placeholder="1234 1234 1234 1234"></p>
<p>Name on card: <input id="nameOnCard"type="text" placeholder="Name on card"></p>
<p> Expiry date:
<input id="expiryMonthAndYear" inputmode="numerical" placeholder="MM / YYYY" type="text" />
</p>
<p><input type="submit" id="submit" value="Submit" aria-label="submit"/></p>
</form>
</body>
</html>
</html>

@ -23,7 +23,9 @@ import java.lang.ref.WeakReference
* deactivate the FxA web channel
* which is not supported on the staging servers.
*/
class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
class AppRequestInterceptor(
private val context: Context,
) : RequestInterceptor {
private var navController: WeakReference<NavController>? = null
@ -86,6 +88,8 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
errorType = improvedErrorType,
uri = uri,
htmlResource = riskLevel.htmlRes,
titleOverride = { type -> getErrorPageTitle(context, type) },
descriptionOverride = { type -> getErrorPageDescription(context, type) },
)
return RequestInterceptor.ErrorResponse(errorPageUri)
@ -125,6 +129,8 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
return null
}
// This method is the only difference from the production code.
// Otherwise the code should be kept identical
@Suppress("LongParameterList")
private fun interceptFxaRequest(
engineSession: EngineSession,
@ -160,6 +166,7 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
return when {
errorType == ErrorType.ERROR_UNKNOWN_HOST && !isConnected -> ErrorType.ERROR_NO_INTERNET
errorType == ErrorType.ERROR_HTTPS_ONLY -> ErrorType.ERROR_HTTPS_ONLY
else -> errorType
}
}
@ -201,6 +208,25 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
-> RiskLevel.High
}
private fun getErrorPageTitle(context: Context, type: ErrorType): String? {
return when (type) {
ErrorType.ERROR_HTTPS_ONLY -> context.getString(R.string.errorpage_httpsonly_title)
// Returning `null` will let the component use its default title for this error type
else -> null
}
}
private fun getErrorPageDescription(context: Context, type: ErrorType): String? {
return when (type) {
ErrorType.ERROR_HTTPS_ONLY ->
context.getString(R.string.errorpage_httpsonly_message_title) +
"<br><br>" +
context.getString(R.string.errorpage_httpsonly_message_summary)
// Returning `null` will let the component use its default description for this error type
else -> null
}
}
internal enum class RiskLevel(val htmlRes: String) {
Low(LOW_AND_MEDIUM_RISK_ERROR_PAGES),
Medium(LOW_AND_MEDIUM_RISK_ERROR_PAGES),

@ -88,5 +88,4 @@ enum class ETPPolicy {
STANDARD,
STRICT,
CUSTOM,
;
}

@ -60,9 +60,13 @@ object MatcherHelper {
}
}
fun assertItemWithDescriptionExists(vararg appItems: UiObject) {
fun assertItemWithDescriptionExists(vararg appItems: UiObject, exists: Boolean = true) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
if (exists) {
assertTrue(appItem.waitForExists(waitingTime))
} else {
assertFalse(appItem.waitForExists(waitingTime))
}
}
}
@ -84,9 +88,13 @@ object MatcherHelper {
}
}
fun assertItemWithResIdAndTextExists(vararg appItems: UiObject) {
fun assertItemWithResIdAndTextExists(vararg appItems: UiObject, exists: Boolean = true) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
if (exists) {
assertTrue(appItem.waitForExists(waitingTime))
} else {
assertFalse(appItem.waitForExists(waitingTime))
}
}
}

@ -113,7 +113,7 @@ object TestHelper {
fun getPermissionAllowID(): String {
return when
(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
true -> "com.android.permissioncontroller"
false -> "com.android.packageinstaller"
}
@ -476,4 +476,14 @@ object TestHelper {
testBlock()
}
}
fun putAppToBackground() {
mDevice.pressRecentApps()
mDevice.findObject(UiSelector().resourceId("$packageName:id/container")).waitUntilGone(waitingTime)
}
fun bringAppToForeground() {
mDevice.pressRecentApps()
mDevice.findObject(UiSelector().resourceId("$packageName:id/container")).waitForExists(waitingTime)
}
}

@ -33,7 +33,7 @@ import org.mozilla.fenix.helpers.HomeActivityTestRule
*
* Say no to main thread IO! 🙅
*/
private const val EXPECTED_SUPPRESSION_COUNT = 18
private const val EXPECTED_SUPPRESSION_COUNT = 17
/**
* The number of times we call the `runBlocking` coroutine method on the main thread during this

@ -26,27 +26,27 @@
},
"attrs": {
"hashes": [
"sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6",
"sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"
"sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836",
"sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"
],
"markers": "python_version >= '3.5'",
"version": "==22.1.0"
"markers": "python_version >= '3.6'",
"version": "==22.2.0"
},
"blessings": {
"blessed": {
"hashes": [
"sha256:98e5854d805f50a5b58ac2333411b0482516a8210f23f43308baeb58d77c157d",
"sha256:b1fdd7e7a675295630f9ae71527a8ebc10bfefa236b3d6aa4932ee4462c17ba3",
"sha256:caad5211e7ba5afe04367cdd4cfc68fa886e2e08f6f35e76b7387d2109ccea6e"
"sha256:0c542922586a265e699188e52d5f5ac5ec0dd517e5a1041d90d2bbf23f906058",
"sha256:2cdd67f8746e048f00df47a2880f4d6acbcdb399031b604e34ba8f71d5787680"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.7"
"markers": "python_version >= '2.7'",
"version": "==1.20.0"
},
"certifi": {
"hashes": [
"sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d",
"sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"
"sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3",
"sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"
],
"version": "==2022.6.15"
"markers": "python_version >= '3.6'",
"version": "==2022.12.7"
},
"cffi": {
"hashes": [
@ -119,46 +119,56 @@
},
"charset-normalizer": {
"hashes": [
"sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5",
"sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"
"sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845",
"sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"
],
"markers": "python_version >= '3.6'",
"version": "==2.1.0"
"version": "==2.1.1"
},
"cryptography": {
"hashes": [
"sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59",
"sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596",
"sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3",
"sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5",
"sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab",
"sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884",
"sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82",
"sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b",
"sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441",
"sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa",
"sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d",
"sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b",
"sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a",
"sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6",
"sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157",
"sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280",
"sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282",
"sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67",
"sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8",
"sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046",
"sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327",
"sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9"
],
"version": "==37.0.4"
"sha256:0f8da300b5c8af9f98111ffd512910bc792b4c77392a9523624680f7956a99d4",
"sha256:35f7c7d015d474f4011e859e93e789c87d21f6f4880ebdc29896a60403328f1f",
"sha256:4789d1e3e257965e960232345002262ede4d094d1a19f4d3b52e48d4d8f3b885",
"sha256:5aa67414fcdfa22cf052e640cb5ddc461924a045cacf325cd164e65312d99502",
"sha256:5d2d8b87a490bfcd407ed9d49093793d0f75198a35e6eb1a923ce1ee86c62b41",
"sha256:6687ef6d0a6497e2b58e7c5b852b53f62142cfa7cd1555795758934da363a965",
"sha256:6f8ba7f0328b79f08bdacc3e4e66fb4d7aab0c3584e0bd41328dce5262e26b2e",
"sha256:706843b48f9a3f9b9911979761c91541e3d90db1ca905fd63fee540a217698bc",
"sha256:807ce09d4434881ca3a7594733669bd834f5b2c6d5c7e36f8c00f691887042ad",
"sha256:83e17b26de248c33f3acffb922748151d71827d6021d98c70e6c1a25ddd78505",
"sha256:96f1157a7c08b5b189b16b47bc9db2332269d6680a196341bf30046330d15388",
"sha256:aec5a6c9864be7df2240c382740fcf3b96928c46604eaa7f3091f58b878c0bb6",
"sha256:b0afd054cd42f3d213bf82c629efb1ee5f22eba35bf0eec88ea9ea7304f511a2",
"sha256:c5caeb8188c24888c90b5108a441c106f7faa4c4c075a2bcae438c6e8ca73cef",
"sha256:ced4e447ae29ca194449a3f1ce132ded8fcab06971ef5f618605aacaa612beac",
"sha256:d1f6198ee6d9148405e49887803907fe8962a23e6c6f83ea7d98f1c0de375695",
"sha256:e124352fd3db36a9d4a21c1aa27fd5d051e621845cb87fb851c08f4f75ce8be6",
"sha256:e422abdec8b5fa8462aa016786680720d78bdce7a30c652b7fadf83a4ba35336",
"sha256:ef8b72fa70b348724ff1218267e7f7375b8de4e8194d1636ee60510aae104cd0",
"sha256:f0c64d1bd842ca2633e74a1a28033d139368ad959872533b1bab8c80e8240a0c",
"sha256:f24077a3b5298a5a06a8e0536e3ea9ec60e4c7ac486755e5fb6e6ea9b3500106",
"sha256:fdd188c8a6ef8769f148f88f859884507b954cc64db6b52f66ef199bb9ad660a",
"sha256:fe913f20024eb2cb2f323e42a64bdf2911bb9738a15dba7d3cce48151034e3a8"
],
"index": "pypi",
"version": "==39.0.1"
},
"distro": {
"hashes": [
"sha256:151aeccf60c216402932b52e40ee477a939f8d58898927378a02abbe852c1c39",
"sha256:d596311d707e692c2160c37807f83e3820c5d539d5a83e87cfb6babd8ba3a06b"
"sha256:02e111d1dc6a50abb8eed6bf31c3e48ed8b0830d1ea2a1b78c61765c2513fdd8",
"sha256:99522ca3e365cac527b44bde033f64c6945d90eb9f769703caaec52b09bbd3ff"
],
"markers": "python_version >= '3.6'",
"version": "==1.7.0"
"version": "==1.8.0"
},
"exceptiongroup": {
"hashes": [
"sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e",
"sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"
],
"markers": "python_version < '3.11'",
"version": "==1.1.0"
},
"fxapom": {
"hashes": [
@ -170,11 +180,11 @@
},
"h11": {
"hashes": [
"sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06",
"sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442"
"sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d",
"sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"
],
"markers": "python_version >= '3.6'",
"version": "==0.13.0"
"markers": "python_version >= '3.7'",
"version": "==0.14.0"
},
"hawkauthlib": {
"hashes": [
@ -185,24 +195,26 @@
},
"idna": {
"hashes": [
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
"sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
"sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
],
"version": "==3.3"
"markers": "python_version >= '3.5'",
"version": "==3.4"
},
"iniconfig": {
"hashes": [
"sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
"sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
"sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
"sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
],
"version": "==1.1.1"
"markers": "python_version >= '3.7'",
"version": "==2.0.0"
},
"mozdevice": {
"hashes": [
"sha256:074ba1ff99b18ccc1931538a161be2410d0f9cee122df852b3bc73e1000fbcad",
"sha256:a5a1e882a72df71165f6322def9b5e1d677d39d25f62157f3e0dc554b5ae04dc"
"sha256:8005df3c77bcb50e7b110ce75310ba11aaab44d550dbbe36d30e82f920ea551a",
"sha256:9a55047998fdffc00c0de2587fd818a5083520b60ce662036a2134515d485f84"
],
"version": "==4.0.3"
"version": "==4.1.0"
},
"mozdownload": {
"hashes": [
@ -214,9 +226,10 @@
},
"mozfile": {
"hashes": [
"sha256:e5dc835582ea150e35ecd57e9d86cb707d3aa3b2505679db7332326dd49fd6b8"
"sha256:3b0afcda2fa8b802ef657df80a56f21619008f61fcc14b756124028d7b7adf5c",
"sha256:92ca1a786abbdf5e6a7aada62d3a4e28f441ef069c7623223add45268e53c789"
],
"version": "==2.1.0"
"version": "==3.0.0"
},
"mozinfo": {
"hashes": [
@ -234,9 +247,10 @@
},
"mozlog": {
"hashes": [
"sha256:54b9a1e781ce31fc10079dc8aec509fff7feca83714edeae6c981e279ceb796f"
"sha256:080c0a7fdf01cc9a3c9dc8dc527ec8adfb447ca0cd05c5f5fe5c944ceeaec3ff",
"sha256:dc389dc861be3fe9ad1db561893a34015935bd203548685000c84ee5177ce05a"
],
"version": "==7.1.0"
"version": "==7.1.1"
},
"mozprocess": {
"hashes": [
@ -286,11 +300,11 @@
},
"packaging": {
"hashes": [
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
"sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2",
"sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"
],
"markers": "python_version >= '3.6'",
"version": "==21.3"
"markers": "python_version >= '3.7'",
"version": "==23.0"
},
"pluggy": {
"hashes": [
@ -302,11 +316,11 @@
},
"progressbar2": {
"hashes": [
"sha256:14d3165a1781d053ffaa117daf27cc706128d2ec1d2977fdb05b6bb079888013",
"sha256:2562ba3e554433f08e81fb7b786208b19de135f3ca1c5da1787d9b05558e6247"
"sha256:1393922fcb64598944ad457569fbeb4b3ac189ef50b5adb9cef3284e87e394ce",
"sha256:1a8e201211f99a85df55f720b3b6da7fb5c8cdef56792c4547205be2de5ea606"
],
"markers": "python_version >= '3.7'",
"version": "==4.0.0"
"version": "==4.2.0"
},
"py": {
"hashes": [
@ -338,33 +352,18 @@
},
"pyjwt": {
"hashes": [
"sha256:72d1d253f32dbd4f5c88eaf1fdc62f3a19f676ccbadb9dbc5d07e951b2b26daf",
"sha256:d42908208c699b3b973cbeb01a969ba6a96c821eefb1c5bfe4c390c01d67abba"
"sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd",
"sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14"
],
"markers": "python_version >= '3.6'",
"version": "==2.4.0"
},
"pyopenssl": {
"hashes": [
"sha256:660b1b1425aac4a1bea1d94168a85d99f0b3144c869dd4390d27629d0087f1bf",
"sha256:ea252b38c87425b64116f808355e8da644ef9b07e429398bfece610f893ee2e0"
],
"version": "==22.0.0"
},
"pyparsing": {
"hashes": [
"sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb",
"sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"
],
"markers": "python_full_version >= '3.6.8'",
"version": "==3.0.9"
"markers": "python_version >= '3.7'",
"version": "==2.6.0"
},
"pypom": {
"hashes": [
"sha256:6f56888d25c1faf8fa0d53ef8a4ba1d7dc828e57212f7fe04e8cd21b5eaa924e",
"sha256:a65297125c498c4e9cee99de9b2ddf5b908d0041ee1fc1e90956d3750483fa3d"
"sha256:5da52cf447e62f43a0cfa47dfe52eb822eff07b2fdad759f930d1d227c15220b",
"sha256:8b4dc6d1a24580298bf5ad8ad6c586f33b73c326c10a4419f83aee1abb20077d"
],
"version": "==2.2.3"
"version": "==2.2.4"
},
"pysocks": {
"hashes": [
@ -408,11 +407,11 @@
},
"python-utils": {
"hashes": [
"sha256:3b1c8b706e40e91280eec5fa72ea730880a166cee99afa5555b863d55664478d",
"sha256:5cb9cf295018202fb4d6abdd694a33a7f08bc0ce1bf1eab8cce80ab2cde35c07"
"sha256:68198854fc276bc4b2403b261703c218e01ef564dcb072a7096ed9ea7aa5130c",
"sha256:8bfefc3430f1c48408fa0e5958eee51d39840a5a987c2181a579e99ab6fe5ca6"
],
"markers": "python_version >= '3.7'",
"version": "==3.3.3"
"version": "==3.5.2"
},
"redo": {
"hashes": [
@ -431,10 +430,19 @@
},
"selenium": {
"hashes": [
"sha256:61c8b02788b66f08f2e61d5295afc956991e394815e33573072e68402ed4e8d5"
"sha256:bd04eb41395605d9b2b65fe587f3fed21431da75512985c52772529e5e210c60",
"sha256:c48372905bffcc3b24bd55ab4683a07ee5e1f30fe918c59558ea5ee44cedf6c3"
],
"markers": "python_version ~= '3.7'",
"version": "==4.4.0"
"markers": "python_version >= '3.7'",
"version": "==4.8.2"
},
"setuptools": {
"hashes": [
"sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330",
"sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251"
],
"markers": "python_version >= '3.7'",
"version": "==67.4.0"
},
"six": {
"hashes": [
@ -446,11 +454,11 @@
},
"sniffio": {
"hashes": [
"sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663",
"sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"
"sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101",
"sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"
],
"markers": "python_version >= '3.5'",
"version": "==1.2.0"
"markers": "python_version >= '3.7'",
"version": "==1.3.0"
},
"sortedcontainers": {
"hashes": [
@ -476,11 +484,11 @@
},
"trio": {
"hashes": [
"sha256:4dc0bf9d5cc78767fc4516325b6d80cc0968705a31d0eec2ecd7cdda466265b0",
"sha256:523f39b7b69eef73501cebfe1aafd400a9aad5b03543a0eded52952488ff1c13"
"sha256:ce68f1c5400a47b137c5a4de72c7c901bd4e7a24fbdebfe9b41de8c6c04eaacf",
"sha256:f1dd0780a89bfc880c7c7994519cb53f62aacb2c25ff487001c0052bd721cdf0"
],
"markers": "python_version >= '3.7'",
"version": "==0.21.0"
"version": "==0.22.0"
},
"trio-websocket": {
"hashes": [
@ -492,15 +500,21 @@
},
"urllib3": {
"extras": [
"secure",
"socks"
],
"hashes": [
"sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72",
"sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==1.26.14"
},
"wcwidth": {
"hashes": [
"sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc",
"sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a"
"sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e",
"sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4.0'",
"version": "==1.26.11"
"version": "==0.2.6"
},
"webob": {
"hashes": [
@ -512,119 +526,110 @@
},
"wsproto": {
"hashes": [
"sha256:2218cb57952d90b9fca325c0dcfb08c3bda93e8fd8070b0a17f048e2e47a521b",
"sha256:a2e56bfd5c7cd83c1369d83b5feccd6d37798b74872866e62616e0ecf111bda8"
"sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065",
"sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"
],
"markers": "python_version >= '3.7'",
"version": "==1.1.0"
"version": "==1.2.0"
},
"zope.component": {
"hashes": [
"sha256:32cbe426ba8fa7b62ce5b211f80f0718a0c749cc7ff09e3f4b43a57f7ccdf5e5",
"sha256:e955eb9f1e55d30e2d8097c8baa9ee012c356887eef3b0d43e6bfcd4868221e5"
"sha256:a508f9fef1b6f5286462d3340cd89ffab5c7899dca0401337239cb6ba7c6bb0a",
"sha256:cbd279e15a959f35a813b64ec4f1027c08b86fcca26f73250c0c912251df90dd"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==5.0.1"
"version": "==5.1.0"
},
"zope.event": {
"hashes": [
"sha256:2666401939cdaa5f4e0c08cf7f20c9b21423b95e88f4675b1443973bdb080c42",
"sha256:5e76517f5b9b119acf37ca8819781db6c16ea433f7e2062c4afc2b6fbedb1330"
"sha256:73d9e3ef750cca14816a9c322c7250b0d7c9dbc337df5d1b807ff8d3d0b9e97c",
"sha256:81d98813046fc86cc4136e3698fee628a3282f9c320db18658c21749235fce80"
],
"version": "==4.5.0"
"version": "==4.6"
},
"zope.hookable": {
"hashes": [
"sha256:031e7672540685dc9d07565f2b968c6e21c7899c9391da58a23e63f229a8fdcd",
"sha256:14bac9afd00e9577227749b37dfc3b9fe4f4fb855923262fc016be47baa42712",
"sha256:16c0748438f716894cb61f3ed00adaa65359a5abe7a12bb44d7133d4b0cd3453",
"sha256:1da8d3386238983e97302b2cbdb510e126968e65e3cb8a8745da9744655001fc",
"sha256:24e4489ccf0dbd4c0e9d2d8b845b46994debb30cc22f96da76e973f9cb582799",
"sha256:3111fbc3ddae5d98655691ff81b1c71bd7ad0c3bcf2c2ef659fdc8e66f664603",
"sha256:34a84e15f2e6a3b2e372a67f7d845a372e3f0438319f26dfe9b7cb66ab75be44",
"sha256:35320d283a364c42dbf91694066bc1e7f07628bbd5ec87c1ae08143036e1de49",
"sha256:43855fbab9fb949fa14b5bbcf4c471e4d359428c2196061efe0d82fe3b387101",
"sha256:4711443b964bf7ace58780a3473f520151de8d441e57583b8d8cd8a231fb7656",
"sha256:49427208d9f65e5aa7df5c1be4dc818c51da34e261f4e52fe944a695ae90e3b3",
"sha256:4dbdfe522741effbb507d55354664c93c81db6db66ca124c981d1e3a41e5a31b",
"sha256:4dc9693fa28a238f063ba169110ef4199e0f86f9f976b57824c1d742eb21ad2e",
"sha256:64337672794b01d039e2380d11dd895ab17043530f27b5e3e0709a11fbe5b532",
"sha256:7067a450b15c8d41d00060890573ddc06f3414e62173dac55b38f6683920c20e",
"sha256:7091df7b4d6d897dd00e8717f0fa62b020e28aabe54a92005df22130ed204f6f",
"sha256:7b63de104e531109f606d4e3765b82fa3364e387d63b71a02460d321e24b33c8",
"sha256:7ef3128b5bcb97029c19df95126f57638afe6d2aca0ffede989149fa0bbd744a",
"sha256:800f93479202b05e46ba7696a2dd5411cfdd9d87c47a7d2b6fcda5394a5f9042",
"sha256:84dfb4f5189c8a3b70dfdf0d268840d5dce69048d4767120910f4665fa5ee2f3",
"sha256:8cf9982d43f6a732d041cf68b08fa969e5bcf090212ab519a637c8a393015eda",
"sha256:8fc3e6cd0486c6af48e3317c299def719b57538332a194e0b3bc6a772f4faa0e",
"sha256:a766186b97e493928dfac4a4b6c7508e47411e78c6f3be9846e8981eb91542d4",
"sha256:bfd1b1107a51ddfbd628c1ff884c6a67e92519b23012563b6082d438dbd16b9d",
"sha256:cbcbf6d31092caafb40ab26d07c065bf4f60f3ba7925d23d51a4e19469034544",
"sha256:cf27270212fa51005162d75cae83bb943c84ece7c7f8d30d68efebfad2f469d9",
"sha256:cf7f1f973aeced06ab1a245faa71e867cec6a053b807c2aad4c1728964aac071",
"sha256:de77a946ef020d08643647e417713ed753a2eed1f4495259c38a241c8eb31dbf",
"sha256:e3b01e7cf16b4a3257ee05e4c354737a4f64af302846826c46a296a7944b8da9",
"sha256:ff08276e555f2ef262fd03d872cca130e7ee376b87d7651a5595aae2fa5b2425"
"sha256:0054539ed839751b7f511193912cba393f0b8b5f7dfe9f3601c65b2d3b74e731",
"sha256:049ef54de127236e555d0864ad3b950b2b6e5048cdf1098cf340c6fb108104c7",
"sha256:06570ed57b22624c7673ff203801bbdece14d2d42dc5d9879c24ef5612c53456",
"sha256:0e9e5adc24954e157e084bee97362346470a06d0305cb095118367a8a776dce4",
"sha256:2e8fd79437c2007020d3faac41e13c49bcbaa6a0738e4142b996c656dcb5bb69",
"sha256:4313b3d282c1c26fcb69569b7988bc2de0b6dc59238ae7189b6b7b29503d47cb",
"sha256:448ca90d78bd3aef75fe5d55d19f5d05a217193738b7a8d5fd9e93ecf2c02c84",
"sha256:4b2fd781571336b0b7655826d9a052379a06b62af138085409b2e3fef1e6fb3d",
"sha256:5215355203b9583b7f2a8f06fa7df272562cc12bf5be1a960a45ea49c3294426",
"sha256:5cb0e4a23588435c6911bde300158d31e47c73c469fbf59d927e801e1cb457ef",
"sha256:71bff8f7c2e223f92a218b0909ccc6f612c075cc3b5ed164cf152f1537cae2ca",
"sha256:7241ab28df7288d9a8bf49339a0aabfbf035b93d6a2a843af13d13dfa735c46a",
"sha256:7269a0fbcd7c5901e255679f8dac835b628eab58d5490c38cf2b15508f181e64",
"sha256:7401bd6138e58231aef751c63718726259a7aa6875d746d8a87bba70271b9cff",
"sha256:761c9bf1b8df6e2b2d5ae87cda27b8e82c33e2f328750e039de4f6f7f35b73cd",
"sha256:78c51f04aabd3b77ba8d3b2c2abaff8b7598376fea7bd1af9929e90549f6dd4c",
"sha256:93cfda0663d4d3db6b1818619fbc14e3df2e703454983c841b3b95894d559f86",
"sha256:9af06ca83ff1ef9f94a98d08095dd8960fc5b71ffc7ed7db05988dc493e148a1",
"sha256:9cffa01d8ef1172492fd6df0113ff5432006129b9bd6e8265e1e4985362b973d",
"sha256:9d398b1de407a5908c8e5f55fb7a26fa177916b1203e697ef0b4c3389dd28e14",
"sha256:9f447ecaf7741257333f4b1cc215de633daaf147dbc87133638142ed88492617",
"sha256:9f5d425eb57dee785e4d32703e45c5d6cf2b9fa7ad37c10214593b5f62daa60b",
"sha256:9f7dd1b45cd13976f49ad21f48a8253628c74ad5eefe3f6e14d50f38cc45f613",
"sha256:9fd11381ec66a8569f999dbe11c94870ddf8aecd591300f203a927f18e938a24",
"sha256:acec917178af910959205f98f48bcd0a165bdcd6b4d8b3f4baf06fa393ac5ff5",
"sha256:b65e86a5cb8244d83eabd021f70968d4a80fac01edc99f6e35d29e5458a128bb",
"sha256:bad033b8adfe71f650fef2d4fc33452b3310a0e53139a530dbffbcf9fe08c8c8",
"sha256:c39ffe1b1ef7543e8efafdc6472d7b9ece8ed1ebe20be261522346463aa2c8c0",
"sha256:c79da9673a7d704f6ea2a4bbef6e5e161adbba9d8371476de28a0e3416510cc1",
"sha256:d06da931ac88ebb4c02ac89d0b6fdb2e4fff130901edf9c6e7ea0338a2edf6bd",
"sha256:d44229a0aa8d3587491f359d7326c55b5db6379f68656785dece792afbcfcbae",
"sha256:d5e50bfbcde1afe32f9cf7fa5e8ea42e218090ecb989c31164d708d0491134b7",
"sha256:d822b7ec71ebb5c96df000e2180127e94ba49258335ae796dc4b6201259b2502",
"sha256:eeb4042f9b1771a1dd8377cb1cb307c4a4f5821d1491becbdc69bc9de66d3918",
"sha256:fb601f00ac87e5aa582a81315ed96768ce3513280729d3f51f79312e2b8b94ac",
"sha256:fd49da3340339b8aeef31153ce898e93867ee5a7ffcf685e903ceae6717f0cc2"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==5.1.0"
"version": "==5.4"
},
"zope.interface": {
"hashes": [
"sha256:08f9636e99a9d5410181ba0729e0408d3d8748026ea938f3b970a0249daa8192",
"sha256:0b465ae0962d49c68aa9733ba92a001b2a0933c317780435f00be7ecb959c702",
"sha256:0cba8477e300d64a11a9789ed40ee8932b59f9ee05f85276dbb4b59acee5dd09",
"sha256:0cee5187b60ed26d56eb2960136288ce91bcf61e2a9405660d271d1f122a69a4",
"sha256:0ea1d73b7c9dcbc5080bb8aaffb776f1c68e807767069b9ccdd06f27a161914a",
"sha256:0f91b5b948686659a8e28b728ff5e74b1be6bf40cb04704453617e5f1e945ef3",
"sha256:15e7d1f7a6ee16572e21e3576d2012b2778cbacf75eb4b7400be37455f5ca8bf",
"sha256:17776ecd3a1fdd2b2cd5373e5ef8b307162f581c693575ec62e7c5399d80794c",
"sha256:194d0bcb1374ac3e1e023961610dc8f2c78a0f5f634d0c737691e215569e640d",
"sha256:1c0e316c9add0db48a5b703833881351444398b04111188069a26a61cfb4df78",
"sha256:205e40ccde0f37496904572035deea747390a8b7dc65146d30b96e2dd1359a83",
"sha256:273f158fabc5ea33cbc936da0ab3d4ba80ede5351babc4f577d768e057651531",
"sha256:2876246527c91e101184f63ccd1d716ec9c46519cc5f3d5375a3351c46467c46",
"sha256:2c98384b254b37ce50eddd55db8d381a5c53b4c10ee66e1e7fe749824f894021",
"sha256:2e5a26f16503be6c826abca904e45f1a44ff275fdb7e9d1b75c10671c26f8b94",
"sha256:334701327f37c47fa628fc8b8d28c7d7730ce7daaf4bda1efb741679c2b087fc",
"sha256:3748fac0d0f6a304e674955ab1365d515993b3a0a865e16a11ec9d86fb307f63",
"sha256:3c02411a3b62668200910090a0dff17c0b25aaa36145082a5a6adf08fa281e54",
"sha256:3dd4952748521205697bc2802e4afac5ed4b02909bb799ba1fe239f77fd4e117",
"sha256:3f24df7124c323fceb53ff6168da70dbfbae1442b4f3da439cd441681f54fe25",
"sha256:469e2407e0fe9880ac690a3666f03eb4c3c444411a5a5fddfdabc5d184a79f05",
"sha256:4de4bc9b6d35c5af65b454d3e9bc98c50eb3960d5a3762c9438df57427134b8e",
"sha256:5208ebd5152e040640518a77827bdfcc73773a15a33d6644015b763b9c9febc1",
"sha256:52de7fc6c21b419078008f697fd4103dbc763288b1406b4562554bd47514c004",
"sha256:5bb3489b4558e49ad2c5118137cfeaf59434f9737fa9c5deefc72d22c23822e2",
"sha256:5dba5f530fec3f0988d83b78cc591b58c0b6eb8431a85edd1569a0539a8a5a0e",
"sha256:5dd9ca406499444f4c8299f803d4a14edf7890ecc595c8b1c7115c2342cadc5f",
"sha256:5f931a1c21dfa7a9c573ec1f50a31135ccce84e32507c54e1ea404894c5eb96f",
"sha256:63b82bb63de7c821428d513607e84c6d97d58afd1fe2eb645030bdc185440120",
"sha256:66c0061c91b3b9cf542131148ef7ecbecb2690d48d1612ec386de9d36766058f",
"sha256:6f0c02cbb9691b7c91d5009108f975f8ffeab5dff8f26d62e21c493060eff2a1",
"sha256:71aace0c42d53abe6fc7f726c5d3b60d90f3c5c055a447950ad6ea9cec2e37d9",
"sha256:7d97a4306898b05404a0dcdc32d9709b7d8832c0c542b861d9a826301719794e",
"sha256:7df1e1c05304f26faa49fa752a8c690126cf98b40b91d54e6e9cc3b7d6ffe8b7",
"sha256:8270252effc60b9642b423189a2fe90eb6b59e87cbee54549db3f5562ff8d1b8",
"sha256:867a5ad16892bf20e6c4ea2aab1971f45645ff3102ad29bd84c86027fa99997b",
"sha256:877473e675fdcc113c138813a5dd440da0769a2d81f4d86614e5d62b69497155",
"sha256:8892f89999ffd992208754851e5a052f6b5db70a1e3f7d54b17c5211e37a98c7",
"sha256:9a9845c4c6bb56e508651f005c4aeb0404e518c6f000d5a1123ab077ab769f5c",
"sha256:a1e6e96217a0f72e2b8629e271e1b280c6fa3fe6e59fa8f6701bec14e3354325",
"sha256:a8156e6a7f5e2a0ff0c5b21d6bcb45145efece1909efcbbbf48c56f8da68221d",
"sha256:a9506a7e80bcf6eacfff7f804c0ad5350c8c95b9010e4356a4b36f5322f09abb",
"sha256:af310ec8335016b5e52cae60cda4a4f2a60a788cbb949a4fbea13d441aa5a09e",
"sha256:b0297b1e05fd128d26cc2460c810d42e205d16d76799526dfa8c8ccd50e74959",
"sha256:bf68f4b2b6683e52bec69273562df15af352e5ed25d1b6641e7efddc5951d1a7",
"sha256:d0c1bc2fa9a7285719e5678584f6b92572a5b639d0e471bb8d4b650a1a910920",
"sha256:d4d9d6c1a455d4babd320203b918ccc7fcbefe308615c521062bc2ba1aa4d26e",
"sha256:db1fa631737dab9fa0b37f3979d8d2631e348c3b4e8325d6873c2541d0ae5a48",
"sha256:dd93ea5c0c7f3e25335ab7d22a507b1dc43976e1345508f845efc573d3d779d8",
"sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4",
"sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263"
"sha256:008b0b65c05993bb08912f644d140530e775cf1c62a072bf9340c2249e613c32",
"sha256:0217a9615531c83aeedb12e126611b1b1a3175013bbafe57c702ce40000eb9a0",
"sha256:0fb497c6b088818e3395e302e426850f8236d8d9f4ef5b2836feae812a8f699c",
"sha256:17ebf6e0b1d07ed009738016abf0d0a0f80388e009d0ac6e0ead26fc162b3b9c",
"sha256:311196634bb9333aa06f00fc94f59d3a9fddd2305c2c425d86e406ddc6f2260d",
"sha256:3218ab1a7748327e08ef83cca63eea7cf20ea7e2ebcb2522072896e5e2fceedf",
"sha256:404d1e284eda9e233c90128697c71acffd55e183d70628aa0bbb0e7a3084ed8b",
"sha256:4087e253bd3bbbc3e615ecd0b6dd03c4e6a1e46d152d3be6d2ad08fbad742dcc",
"sha256:40f4065745e2c2fa0dff0e7ccd7c166a8ac9748974f960cd39f63d2c19f9231f",
"sha256:5334e2ef60d3d9439c08baedaf8b84dc9bb9522d0dacbc10572ef5609ef8db6d",
"sha256:604cdba8f1983d0ab78edc29aa71c8df0ada06fb147cea436dc37093a0100a4e",
"sha256:6373d7eb813a143cb7795d3e42bd8ed857c82a90571567e681e1b3841a390d16",
"sha256:655796a906fa3ca67273011c9805c1e1baa047781fca80feeb710328cdbed87f",
"sha256:65c3c06afee96c654e590e046c4a24559e65b0a87dbff256cd4bd6f77e1a33f9",
"sha256:696f3d5493eae7359887da55c2afa05acc3db5fc625c49529e84bd9992313296",
"sha256:6e972493cdfe4ad0411fd9abfab7d4d800a7317a93928217f1a5de2bb0f0d87a",
"sha256:7579960be23d1fddecb53898035a0d112ac858c3554018ce615cefc03024e46d",
"sha256:765d703096ca47aa5d93044bf701b00bbce4d903a95b41fff7c3796e747b1f1d",
"sha256:7e66f60b0067a10dd289b29dceabd3d0e6d68be1504fc9d0bc209cf07f56d189",
"sha256:8a2ffadefd0e7206adc86e492ccc60395f7edb5680adedf17a7ee4205c530df4",
"sha256:959697ef2757406bff71467a09d940ca364e724c534efbf3786e86eee8591452",
"sha256:9d783213fab61832dbb10d385a319cb0e45451088abd45f95b5bb88ed0acca1a",
"sha256:a16025df73d24795a0bde05504911d306307c24a64187752685ff6ea23897cb0",
"sha256:a2ad597c8c9e038a5912ac3cf166f82926feff2f6e0dabdab956768de0a258f5",
"sha256:bfee1f3ff62143819499e348f5b8a7f3aa0259f9aca5e0ddae7391d059dce671",
"sha256:d169ccd0756c15bbb2f1acc012f5aab279dffc334d733ca0d9362c5beaebe88e",
"sha256:d514c269d1f9f5cd05ddfed15298d6c418129f3f064765295659798349c43e6f",
"sha256:d692374b578360d36568dd05efb8a5a67ab6d1878c29c582e37ddba80e66c396",
"sha256:dbaeb9cf0ea0b3bc4b36fae54a016933d64c6d52a94810a63c00f440ecb37dd7",
"sha256:dc26c8d44472e035d59d6f1177eb712888447f5799743da9c398b0339ed90b1b",
"sha256:e1574980b48c8c74f83578d1e77e701f8439a5d93f36a5a0af31337467c08fcf",
"sha256:e74a578172525c20d7223eac5f8ad187f10940dac06e40113d62f14f3adb1e8f",
"sha256:e945de62917acbf853ab968d8916290548df18dd62c739d862f359ecd25842a6",
"sha256:f0980d44b8aded808bec5059018d64692f0127f10510eca71f2f0ace8fb11188",
"sha256:f98d4bd7bbb15ca701d19b93263cc5edfd480c3475d163f137385f49e5b3a3a7",
"sha256:fb68d212efd057596dee9e6582daded9f8ef776538afdf5feceb3059df2d2e7b"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==5.4.0"
"version": "==5.5.2"
}
},
"develop": {}

@ -0,0 +1,111 @@
package org.mozilla.fenix.ui
import androidx.core.net.toUri
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.searchScreen
class AddToHomeScreenTest {
private lateinit var mockWebServer: MockWebServer
private val downloadTestPage =
"https://storage.googleapis.com/mobile_test_assets/test_app/downloads.html"
private val pdfFileName = "washington.pdf"
private val pdfFileURL = "storage.googleapis.com/mobile_test_assets/public/washington.pdf"
private val pdfFileContent = "Washington Crossing the Delaware"
@get:Rule
val activityIntentTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
@Before
fun setUp() {
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
// Verifies the Add to home screen option in a tab's 3 dot menu
@SmokeTest
@Test
fun mainMenuAddToHomeScreenTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val shortcutTitle = TestHelper.generateRandomString(5)
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
}.openThreeDotMenu {
expandMenu()
}.openAddToHomeScreen {
clickCancelShortcutButton()
}
browserScreen {
}.openThreeDotMenu {
expandMenu()
}.openAddToHomeScreen {
verifyShortcutTextFieldTitle("Test_Page_1")
addShortcutName(shortcutTitle)
clickAddShortcutButton()
clickAddAutomaticallyButton()
}.openHomeScreenShortcut(shortcutTitle) {
verifyUrl(website.url.toString())
verifyTabCounter("1")
}
}
@SmokeTest
@Test
fun addPrivateBrowsingShortcutTest() {
homeScreen {
}.dismissOnboarding()
homeScreen {
}.triggerPrivateBrowsingShortcutPrompt {
verifyNoThanksPrivateBrowsingShortcutButton()
verifyAddPrivateBrowsingShortcutButton()
clickAddPrivateBrowsingShortcutButton()
clickAddAutomaticallyButton()
}.openHomeScreenShortcut("Private ${TestHelper.appName}") {}
searchScreen {
verifySearchView()
}.dismissSearchBar {
verifyCommonMythsLink()
}
}
@SmokeTest
@Test
fun addPDFToHomeScreenTest() {
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
clickLinkMatchingText(pdfFileName)
verifyUrl(pdfFileURL)
verifyPageContent(pdfFileContent)
}.openThreeDotMenu {
expandMenu()
}.openAddToHomeScreen {
verifyShortcutTextFieldTitle(pdfFileName)
clickAddShortcutButton()
clickAddAutomaticallyButton()
}.openHomeScreenShortcut(pdfFileName) {
verifyUrl(pdfFileURL)
}
}
}

@ -109,7 +109,7 @@ class CollectionTest {
homeScreen {
verifyCollectionIsDisplayed(collectionName)
}.expandCollection(collectionName, composeTestRule) {
}.expandCollection(collectionName) {
verifyTabSavedInCollection(webPage.title)
verifyCollectionTabUrl(true, webPageUrl)
verifyShareCollectionButtonIsVisible(true)
@ -126,7 +126,8 @@ class CollectionTest {
}
homeScreen {
}.expandCollection(collectionName, composeTestRule) {
verifyCollectionIsDisplayed(collectionName)
}.expandCollection(collectionName) {
verifyTabSavedInCollection(webPage.title)
verifyCollectionTabUrl(true, webPageUrl)
verifyShareCollectionButtonIsVisible(true)
@ -166,7 +167,8 @@ class CollectionTest {
}
homeScreen {
}.expandCollection(collectionName, composeTestRule) {
verifyCollectionIsDisplayed(collectionName)
}.expandCollection(collectionName) {
clickCollectionThreeDotButton(composeTestRule)
selectOpenTabs(composeTestRule)
}
@ -194,7 +196,8 @@ class CollectionTest {
verifySnackBarText("Collection saved!")
}.openTabsListThreeDotMenu {
}.closeAllTabs {
}.expandCollection(collectionName, composeTestRule) {
verifyCollectionIsDisplayed(collectionName)
}.expandCollection(collectionName) {
}.clickShareCollectionButton {
verifyShareTabsOverlay(firstWebsite.title, secondWebsite.title)
verifySharingWithSelectedApp(sharingApp, urlString, collectionName)
@ -216,7 +219,8 @@ class CollectionTest {
}
homeScreen {
}.expandCollection(collectionName, composeTestRule) {
verifyCollectionIsDisplayed(collectionName)
}.expandCollection(collectionName) {
clickCollectionThreeDotButton(composeTestRule)
selectDeleteCollection(composeTestRule)
}
@ -248,7 +252,8 @@ class CollectionTest {
}.selectExistingCollection(collectionName) {
verifySnackBarText("Tab saved!")
}.goToHomescreen {
}.expandCollection(collectionName, composeTestRule) {
verifyCollectionIsDisplayed(collectionName)
}.expandCollection(collectionName) {
verifyTabSavedInCollection(firstWebPage.title)
verifyTabSavedInCollection(secondWebPage.title)
}
@ -270,7 +275,8 @@ class CollectionTest {
navigationToolbar {
}.enterURLAndEnterToBrowser(secondWebPage.url) {
}.goToHomescreen {
}.expandCollection(collectionName, composeTestRule) {
verifyCollectionIsDisplayed(collectionName)
}.expandCollection(collectionName) {
clickCollectionThreeDotButton(composeTestRule)
selectAddTabToCollection(composeTestRule)
verifyTabsSelectedCounterText(1)
@ -291,10 +297,12 @@ class CollectionTest {
verifySnackBarText("Collection saved!")
}.closeTabDrawer {
}.goToHomescreen {
}.expandCollection(firstCollectionName, composeTestRule) {
verifyCollectionIsDisplayed(firstCollectionName)
}.expandCollection(firstCollectionName) {
clickCollectionThreeDotButton(composeTestRule)
selectRenameCollection(composeTestRule)
}.typeCollectionNameAndSave(secondCollectionName) {}
homeScreen {
verifyCollectionIsDisplayed(secondCollectionName)
}
@ -334,7 +342,8 @@ class CollectionTest {
}
homeScreen {
}.expandCollection(collectionName, composeTestRule) {
verifyCollectionIsDisplayed(collectionName)
}.expandCollection(collectionName) {
verifyTabSavedInCollection(webPage.title, true)
removeTabFromCollection(webPage.title)
verifyTabSavedInCollection(webPage.title, false)
@ -360,8 +369,8 @@ class CollectionTest {
}
homeScreen {
}.expandCollection(collectionName, composeTestRule) {
swipeToBottom()
verifyCollectionIsDisplayed(collectionName)
}.expandCollection(collectionName) {
swipeTabLeft(testPage.title, composeTestRule)
verifyTabSavedInCollection(testPage.title, false)
}
@ -386,8 +395,8 @@ class CollectionTest {
}
homeScreen {
}.expandCollection(collectionName, composeTestRule) {
swipeToBottom()
verifyCollectionIsDisplayed(collectionName)
}.expandCollection(collectionName) {
swipeTabRight(testPage.title, composeTestRule)
verifyTabSavedInCollection(testPage.title, false)
}
@ -422,7 +431,7 @@ class CollectionTest {
tabDrawer {
}.closeTabDrawer {
}.goToHomescreen {
}.expandCollection(collectionName, composeTestRule) {
}.expandCollection(collectionName) {
verifyTabSavedInCollection(firstWebPage.title)
verifyTabSavedInCollection(secondWebPage.title)
}
@ -473,7 +482,8 @@ class CollectionTest {
}
homeScreen {
}.expandCollection(collectionName, composeTestRule) {
verifyCollectionIsDisplayed(collectionName)
}.expandCollection(collectionName) {
clickCollectionThreeDotButton(composeTestRule)
selectDeleteCollection(composeTestRule)
}

@ -0,0 +1,106 @@
package org.mozilla.fenix.ui
import androidx.core.net.toUri
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.exitMenu
import org.mozilla.fenix.helpers.TestHelper.restartApp
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
class CookieBannerReductionTest {
@get:Rule
val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
@SmokeTest
@Test
fun verifyCookieBannerReductionTest() {
val webSite = "voetbal24.be"
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(webSite.toUri()) {
waitForPageToLoad()
verifyCookieBannerExists(exists = true)
}.openThreeDotMenu {
}.openSettings {
}.openCookieBannerReductionSubMenu {
verifyCookieBannerView(isCookieBannerReductionChecked = false)
clickCookieBannerReductionToggle()
verifyCheckedCookieBannerReductionToggle(isCookieBannerReductionChecked = true)
}
exitMenu()
browserScreen {
verifyCookieBannerExists(exists = false)
}
TestHelper.restartApp(activityTestRule)
browserScreen {
verifyCookieBannerExists(exists = false)
}.openThreeDotMenu {
}.openSettings {
}.openCookieBannerReductionSubMenu {
clickCookieBannerReductionToggle()
verifyCheckedCookieBannerReductionToggle(false)
}
exitMenu()
browserScreen {
}.openThreeDotMenu {
}.refreshPage {
verifyCookieBannerExists(exists = false)
}
}
@SmokeTest
@Test
fun verifyCookieBannerReductionInPrivateBrowsingTest() {
val webSite = "voetbal24.be"
homeScreen {
}.togglePrivateBrowsingMode()
navigationToolbar {
}.enterURLAndEnterToBrowser(webSite.toUri()) {
waitForPageToLoad()
verifyCookieBannerExists(exists = true)
}.openThreeDotMenu {
}.openSettings {
}.openCookieBannerReductionSubMenu {
verifyCookieBannerView(isCookieBannerReductionChecked = false)
clickCookieBannerReductionToggle()
verifyCheckedCookieBannerReductionToggle(isCookieBannerReductionChecked = true)
exitMenu()
}
browserScreen {
verifyCookieBannerExists(exists = false)
}
restartApp(activityTestRule)
homeScreen {
}.openTabDrawer {
}.openTab("Voetbal24") {
verifyCookieBannerExists(exists = false)
}.openThreeDotMenu {
}.openSettings {
}.openCookieBannerReductionSubMenu {
clickCookieBannerReductionToggle()
verifyCheckedCookieBannerReductionToggle(false)
exitMenu()
}
browserScreen {
}.openThreeDotMenu {
}.refreshPage {
verifyCookieBannerExists(exists = false)
}
}
}

@ -130,7 +130,7 @@ class CrashReportingTest {
verifyExistingOpenTabs(secondWebPage.title)
}.closeTabDrawer {
}.goToHomescreen {
verifyPrivateSessionMessage()
verifyCommonMythsLink()
}.openThreeDotMenu {
verifySettingsButton()
}

@ -0,0 +1,639 @@
package org.mozilla.fenix.ui
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.bringAppToForeground
import org.mozilla.fenix.helpers.TestHelper.exitMenu
import org.mozilla.fenix.helpers.TestHelper.putAppToBackground
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
import java.time.LocalDate
class CreditCardAutofillTest {
private lateinit var mockWebServer: MockWebServer
object MockCreditCard1 {
const val MOCK_CREDIT_CARD_NUMBER = "5555555555554444"
const val MOCK_LAST_CARD_DIGITS = "4444"
const val MOCK_NAME_ON_CARD = "Mastercard"
const val MOCK_EXPIRATION_MONTH = "February"
val MOCK_EXPIRATION_YEAR = (LocalDate.now().year + 1).toString()
val MOCK_EXPIRATION_MONTH_AND_YEAR = "02/${(LocalDate.now().year + 1)}"
}
object MockCreditCard2 {
const val MOCK_CREDIT_CARD_NUMBER = "2720994326581252"
const val MOCK_LAST_CARD_DIGITS = "1252"
const val MOCK_NAME_ON_CARD = "Mastercard"
const val MOCK_EXPIRATION_MONTH = "March"
val MOCK_EXPIRATION_YEAR = (LocalDate.now().year + 2).toString()
val MOCK_EXPIRATION_MONTH_AND_YEAR = "03/${(LocalDate.now().year + 2)}"
}
@get:Rule
val activityIntentTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
@Before
fun setUp() {
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
@SmokeTest
@Test
fun verifyCreditCardAutofillTest() {
val creditCardFormPage = TestAssetHelper.getCreditCardFormAsset(mockWebServer)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openAutofillSubMenu {
clickAddCreditCardButton()
fillAndSaveCreditCard(
MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
MockCreditCard1.MOCK_NAME_ON_CARD,
MockCreditCard1.MOCK_EXPIRATION_MONTH,
MockCreditCard1.MOCK_EXPIRATION_YEAR,
)
// Opening Manage saved cards to dismiss here the Secure your credit prompt
clickManageSavedCreditCardsButton()
clickSecuredCreditCardsLaterButton()
}.goBackToAutofillSettings {
}.goBack {
}.goBack {
}
navigationToolbar {
}.enterURLAndEnterToBrowser(creditCardFormPage.url) {
clickCreditCardNumberTextBox()
clickSelectCreditCardButton()
clickCreditCardSuggestion(MockCreditCard1.MOCK_LAST_CARD_DIGITS)
verifyAutofilledCreditCard(MockCreditCard1.MOCK_CREDIT_CARD_NUMBER)
}
}
@SmokeTest
@Test
fun deleteSavedCreditCardUsingToolbarButtonTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openAutofillSubMenu {
clickAddCreditCardButton()
fillAndSaveCreditCard(
MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
MockCreditCard1.MOCK_NAME_ON_CARD,
MockCreditCard1.MOCK_EXPIRATION_MONTH,
MockCreditCard1.MOCK_EXPIRATION_YEAR,
)
clickManageSavedCreditCardsButton()
clickSecuredCreditCardsLaterButton()
clickSavedCreditCard()
clickDeleteCreditCardToolbarButton()
clickConfirmDeleteCreditCardButton()
verifyAddCreditCardsButton()
}
}
@SmokeTest
@Test
fun cancelDeleteSavedCreditCardUsingToolbarButtonTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openAutofillSubMenu {
clickAddCreditCardButton()
fillAndSaveCreditCard(
MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
MockCreditCard1.MOCK_NAME_ON_CARD,
MockCreditCard1.MOCK_EXPIRATION_MONTH,
MockCreditCard1.MOCK_EXPIRATION_YEAR,
)
clickManageSavedCreditCardsButton()
clickSecuredCreditCardsLaterButton()
clickSavedCreditCard()
clickDeleteCreditCardToolbarButton()
clickCancelDeleteCreditCardButton()
verifyEditCreditCardToolbarTitle()
}
}
@SmokeTest
@Test
fun deleteSavedCreditCardUsingMenuButtonTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openAutofillSubMenu {
clickAddCreditCardButton()
fillAndSaveCreditCard(
MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
MockCreditCard1.MOCK_NAME_ON_CARD,
MockCreditCard1.MOCK_EXPIRATION_MONTH,
MockCreditCard1.MOCK_EXPIRATION_YEAR,
)
clickManageSavedCreditCardsButton()
clickSecuredCreditCardsLaterButton()
clickSavedCreditCard()
clickDeleteCreditCardMenuButton()
clickConfirmDeleteCreditCardButton()
verifyAddCreditCardsButton()
}
}
@SmokeTest
@Test
fun cancelDeleteSavedCreditCardUsingMenuButtonTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openAutofillSubMenu {
clickAddCreditCardButton()
fillAndSaveCreditCard(
MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
MockCreditCard1.MOCK_NAME_ON_CARD,
MockCreditCard1.MOCK_EXPIRATION_MONTH,
MockCreditCard1.MOCK_EXPIRATION_YEAR,
)
clickManageSavedCreditCardsButton()
clickSecuredCreditCardsLaterButton()
clickSavedCreditCard()
clickDeleteCreditCardMenuButton()
clickCancelDeleteCreditCardButton()
verifyEditCreditCardToolbarTitle()
}
}
@Test
fun verifyCreditCardsSectionTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openAutofillSubMenu {
verifyCreditCardsAutofillSection(true, false)
clickAddCreditCardButton()
fillAndSaveCreditCard(
MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
MockCreditCard1.MOCK_NAME_ON_CARD,
MockCreditCard1.MOCK_EXPIRATION_MONTH,
MockCreditCard1.MOCK_EXPIRATION_YEAR,
)
clickManageSavedCreditCardsButton()
clickSecuredCreditCardsLaterButton()
verifySavedCreditCardsSection(
MockCreditCard1.MOCK_LAST_CARD_DIGITS,
MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR,
)
}
}
@Test
fun verifyManageCreditCardsPromptOptionTest() {
val creditCardFormPage = TestAssetHelper.getCreditCardFormAsset(mockWebServer)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openAutofillSubMenu {
clickAddCreditCardButton()
fillAndSaveCreditCard(
MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
MockCreditCard1.MOCK_NAME_ON_CARD,
MockCreditCard1.MOCK_EXPIRATION_MONTH,
MockCreditCard1.MOCK_EXPIRATION_YEAR,
)
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(creditCardFormPage.url) {
clickCreditCardNumberTextBox()
clickSelectCreditCardButton()
}.clickManageCreditCardsButton {
}.goBackToBrowser {
verifySelectCreditCardPromptExists(false)
}
}
@Test
fun verifyCreditCardsAutofillToggleTest() {
val creditCardFormPage = TestAssetHelper.getCreditCardFormAsset(mockWebServer)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openAutofillSubMenu {
verifyCreditCardsAutofillSection(true, false)
clickAddCreditCardButton()
fillAndSaveCreditCard(
MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
MockCreditCard1.MOCK_NAME_ON_CARD,
MockCreditCard1.MOCK_EXPIRATION_MONTH,
MockCreditCard1.MOCK_EXPIRATION_YEAR,
)
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(creditCardFormPage.url) {
clickCreditCardNumberTextBox()
verifySelectCreditCardPromptExists(true)
}.openThreeDotMenu {
}.openSettings {
}.openAutofillSubMenu {
clickSaveAndAutofillCreditCardsOption()
verifyCreditCardsAutofillSection(false, true)
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(creditCardFormPage.url) {
clickCreditCardNumberTextBox()
verifySelectCreditCardPromptExists(false)
}
}
@Test
fun verifyEditCardsViewTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openAutofillSubMenu {
verifyCreditCardsAutofillSection(true, false)
clickAddCreditCardButton()
fillAndSaveCreditCard(
MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
MockCreditCard1.MOCK_NAME_ON_CARD,
MockCreditCard1.MOCK_EXPIRATION_MONTH,
MockCreditCard1.MOCK_EXPIRATION_YEAR,
)
clickManageSavedCreditCardsButton()
clickSecuredCreditCardsLaterButton()
verifySavedCreditCardsSection(
MockCreditCard1.MOCK_LAST_CARD_DIGITS,
MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR,
)
clickSavedCreditCard()
verifyEditCreditCardView(
MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
MockCreditCard1.MOCK_NAME_ON_CARD,
MockCreditCard1.MOCK_EXPIRATION_MONTH,
MockCreditCard1.MOCK_EXPIRATION_YEAR,
)
}.goBackToSavedCreditCards {
verifySavedCreditCardsSection(
MockCreditCard1.MOCK_LAST_CARD_DIGITS,
MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR,
)
}
}
@Test
fun verifyEditedCardIsSavedTest() {
val creditCardFormPage = TestAssetHelper.getCreditCardFormAsset(mockWebServer)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openAutofillSubMenu {
verifyCreditCardsAutofillSection(true, false)
clickAddCreditCardButton()
fillAndSaveCreditCard(
MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
MockCreditCard1.MOCK_NAME_ON_CARD,
MockCreditCard1.MOCK_EXPIRATION_MONTH,
MockCreditCard1.MOCK_EXPIRATION_YEAR,
)
clickManageSavedCreditCardsButton()
clickSecuredCreditCardsLaterButton()
verifySavedCreditCardsSection(
MockCreditCard1.MOCK_LAST_CARD_DIGITS,
MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR,
)
clickSavedCreditCard()
fillAndSaveCreditCard(
MockCreditCard2.MOCK_CREDIT_CARD_NUMBER,
MockCreditCard2.MOCK_NAME_ON_CARD,
MockCreditCard2.MOCK_EXPIRATION_MONTH,
MockCreditCard2.MOCK_EXPIRATION_YEAR,
)
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(creditCardFormPage.url) {
clickCreditCardNumberTextBox()
clickSelectCreditCardButton()
clickCreditCardSuggestion(MockCreditCard2.MOCK_LAST_CARD_DIGITS)
verifyAutofilledCreditCard(MockCreditCard2.MOCK_CREDIT_CARD_NUMBER)
}
}
@Test
fun verifyCreditCardCannotBeSavedWithoutCardNumberTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openAutofillSubMenu {
verifyCreditCardsAutofillSection(true, false)
clickAddCreditCardButton()
fillAndSaveCreditCard(
MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
MockCreditCard1.MOCK_NAME_ON_CARD,
MockCreditCard1.MOCK_EXPIRATION_MONTH,
MockCreditCard1.MOCK_EXPIRATION_YEAR,
)
clickManageSavedCreditCardsButton()
clickSecuredCreditCardsLaterButton()
verifySavedCreditCardsSection(
MockCreditCard1.MOCK_LAST_CARD_DIGITS,
MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR,
)
clickSavedCreditCard()
clearCreditCardNumber()
clickSaveCreditCardToolbarButton()
verifyEditCreditCardToolbarTitle()
verifyCreditCardNumberErrorMessage()
}
}
@Test
fun verifyCreditCardCannotBeSavedWithoutNameOnCardTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openAutofillSubMenu {
verifyCreditCardsAutofillSection(true, false)
clickAddCreditCardButton()
fillAndSaveCreditCard(
MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
MockCreditCard1.MOCK_NAME_ON_CARD,
MockCreditCard1.MOCK_EXPIRATION_MONTH,
MockCreditCard1.MOCK_EXPIRATION_YEAR,
)
clickManageSavedCreditCardsButton()
clickSecuredCreditCardsLaterButton()
verifySavedCreditCardsSection(
MockCreditCard1.MOCK_LAST_CARD_DIGITS,
MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR,
)
clickSavedCreditCard()
clearNameOnCreditCard()
clickSaveCreditCardToolbarButton()
verifyEditCreditCardToolbarTitle()
verifyNameOnCreditCardErrorMessage()
}
}
@Test
fun verifyMultipleCreditCardsCanBeSavedTest() {
val creditCardFormPage = TestAssetHelper.getCreditCardFormAsset(mockWebServer)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openAutofillSubMenu {
verifyCreditCardsAutofillSection(true, false)
clickAddCreditCardButton()
fillAndSaveCreditCard(
MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
MockCreditCard1.MOCK_NAME_ON_CARD,
MockCreditCard1.MOCK_EXPIRATION_MONTH,
MockCreditCard1.MOCK_EXPIRATION_YEAR,
)
clickManageSavedCreditCardsButton()
clickSecuredCreditCardsLaterButton()
clickAddCreditCardButton()
fillAndSaveCreditCard(
MockCreditCard2.MOCK_CREDIT_CARD_NUMBER,
MockCreditCard2.MOCK_NAME_ON_CARD,
MockCreditCard2.MOCK_EXPIRATION_MONTH,
MockCreditCard2.MOCK_EXPIRATION_YEAR,
)
verifySavedCreditCardsSection(
MockCreditCard1.MOCK_LAST_CARD_DIGITS,
MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR,
)
verifySavedCreditCardsSection(
MockCreditCard2.MOCK_LAST_CARD_DIGITS,
MockCreditCard2.MOCK_EXPIRATION_MONTH_AND_YEAR,
)
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(creditCardFormPage.url) {
clickCreditCardNumberTextBox()
clickSelectCreditCardButton()
verifyCreditCardSuggestion(
MockCreditCard1.MOCK_LAST_CARD_DIGITS,
MockCreditCard2.MOCK_LAST_CARD_DIGITS,
)
clickCreditCardSuggestion(MockCreditCard2.MOCK_LAST_CARD_DIGITS)
verifyAutofilledCreditCard(MockCreditCard2.MOCK_CREDIT_CARD_NUMBER)
}
}
@Test
fun verifyDoNotSaveCreditCardFromFormTest() {
val creditCardFormPage = TestAssetHelper.getCreditCardFormAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(creditCardFormPage.url) {
fillAndSaveCreditCard(
MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
MockCreditCard1.MOCK_NAME_ON_CARD,
MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR,
)
clickCancelCreditCardPromptButton()
verifyUpdateOrSaveCreditCardPromptExists(exists = false)
}.openThreeDotMenu {
}.openSettings {
}.openAutofillSubMenu {
verifyCreditCardsAutofillSection(true, false)
}
}
@Test
fun verifySaveCreditCardFromFormTest() {
val creditCardFormPage = TestAssetHelper.getCreditCardFormAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(creditCardFormPage.url) {
fillAndSaveCreditCard(
MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
MockCreditCard1.MOCK_NAME_ON_CARD,
MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR,
)
clickUpdateOrSaveCreditCardPromptButton()
verifyUpdateOrSaveCreditCardPromptExists(exists = false)
}.openThreeDotMenu {
}.openSettings {
}.openAutofillSubMenu {
verifyCreditCardsAutofillSection(true, true)
clickManageSavedCreditCardsButton()
clickSecuredCreditCardsLaterButton()
verifySavedCreditCardsSection(
MockCreditCard1.MOCK_LAST_CARD_DIGITS,
MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR,
)
}
}
@Test
fun verifyCancelCreditCardUpdatePromptTest() {
val creditCardFormPage = TestAssetHelper.getCreditCardFormAsset(mockWebServer)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openAutofillSubMenu {
verifyCreditCardsAutofillSection(true, false)
clickAddCreditCardButton()
fillAndSaveCreditCard(
MockCreditCard2.MOCK_CREDIT_CARD_NUMBER,
MockCreditCard2.MOCK_NAME_ON_CARD,
MockCreditCard2.MOCK_EXPIRATION_MONTH,
MockCreditCard2.MOCK_EXPIRATION_YEAR,
)
// Opening Manage saved cards to dismiss here the Secure your credit prompt
clickManageSavedCreditCardsButton()
clickSecuredCreditCardsLaterButton()
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(creditCardFormPage.url) {
clickCreditCardNumberTextBox()
clickSelectCreditCardButton()
clickCreditCardSuggestion(MockCreditCard2.MOCK_LAST_CARD_DIGITS)
verifyAutofilledCreditCard(MockCreditCard2.MOCK_CREDIT_CARD_NUMBER)
changeCreditCardExpiryDate(MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR)
clickCreditCardFormSubmitButton()
clickCancelCreditCardPromptButton()
verifyUpdateOrSaveCreditCardPromptExists(false)
}.openThreeDotMenu {
}.openSettings {
}.openAutofillSubMenu {
verifyCreditCardsAutofillSection(true, true)
clickManageSavedCreditCardsButton()
verifySavedCreditCardsSection(
MockCreditCard2.MOCK_LAST_CARD_DIGITS,
MockCreditCard2.MOCK_EXPIRATION_MONTH_AND_YEAR,
)
}
}
@Test
fun verifyConfirmCreditCardUpdatePromptTest() {
val creditCardFormPage = TestAssetHelper.getCreditCardFormAsset(mockWebServer)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openAutofillSubMenu {
verifyCreditCardsAutofillSection(true, false)
clickAddCreditCardButton()
fillAndSaveCreditCard(
MockCreditCard2.MOCK_CREDIT_CARD_NUMBER,
MockCreditCard2.MOCK_NAME_ON_CARD,
MockCreditCard2.MOCK_EXPIRATION_MONTH,
MockCreditCard2.MOCK_EXPIRATION_YEAR,
)
// Opening Manage saved cards to dismiss here the Secure your credit prompt
clickManageSavedCreditCardsButton()
clickSecuredCreditCardsLaterButton()
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(creditCardFormPage.url) {
clickCreditCardNumberTextBox()
clickSelectCreditCardButton()
clickCreditCardSuggestion(MockCreditCard2.MOCK_LAST_CARD_DIGITS)
verifyAutofilledCreditCard(MockCreditCard2.MOCK_CREDIT_CARD_NUMBER)
changeCreditCardExpiryDate(MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR)
clickCreditCardFormSubmitButton()
clickUpdateOrSaveCreditCardPromptButton()
verifyUpdateOrSaveCreditCardPromptExists(false)
}.openThreeDotMenu {
}.openSettings {
}.openAutofillSubMenu {
verifyCreditCardsAutofillSection(true, true)
clickManageSavedCreditCardsButton()
verifySavedCreditCardsSection(
MockCreditCard2.MOCK_LAST_CARD_DIGITS,
MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR,
)
}
}
@Test
fun verifySavedCreditCardsRedirectionToAutofillAfterInterruptionTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openAutofillSubMenu {
verifyCreditCardsAutofillSection(true, false)
clickAddCreditCardButton()
fillAndSaveCreditCard(
MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
MockCreditCard1.MOCK_NAME_ON_CARD,
MockCreditCard1.MOCK_EXPIRATION_MONTH,
MockCreditCard1.MOCK_EXPIRATION_YEAR,
)
clickManageSavedCreditCardsButton()
clickSecuredCreditCardsLaterButton()
verifySavedCreditCardsSection(
MockCreditCard1.MOCK_LAST_CARD_DIGITS,
MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR,
)
putAppToBackground()
bringAppToForeground()
verifyAutofillToolbarTitle()
}
}
@Test
fun verifyEditCreditCardRedirectionToAutofillAfterInterruptionTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openAutofillSubMenu {
verifyCreditCardsAutofillSection(true, false)
clickAddCreditCardButton()
fillAndSaveCreditCard(
MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
MockCreditCard1.MOCK_NAME_ON_CARD,
MockCreditCard1.MOCK_EXPIRATION_MONTH,
MockCreditCard1.MOCK_EXPIRATION_YEAR,
)
clickManageSavedCreditCardsButton()
clickSecuredCreditCardsLaterButton()
clickSavedCreditCard()
putAppToBackground()
bringAppToForeground()
verifyAutofillToolbarTitle()
}
}
}

@ -5,19 +5,16 @@
package org.mozilla.fenix.ui
import androidx.core.net.toUri
import mozilla.components.concept.engine.utils.EngineReleaseChannel
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.components
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestHelper.assertExternalAppOpens
import org.mozilla.fenix.helpers.TestHelper.deleteDownloadedFileOnStorage
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.runWithCondition
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.downloadRobot
import org.mozilla.fenix.ui.robots.navigationToolbar
@ -113,6 +110,11 @@ class DownloadTest {
@SmokeTest
@Test
fun pauseResumeCancelDownloadTest() {
// Clear the "Firefox Fenix default browser notification"
notificationShade {
cancelAllShownNotifications()
}
downloadFile = "1GB.zip"
navigationToolbar {
@ -170,6 +172,7 @@ class DownloadTest {
}
}
@Ignore("failing, see https://bugzilla.mozilla.org/show_bug.cgi?id=1821024")
@SmokeTest
@Test
fun openDownloadedFileTest() {
@ -196,42 +199,28 @@ class DownloadTest {
@SmokeTest
@Test
fun openPDFInBrowserTest() {
runWithCondition(
// Returns the GeckoView channel set for the current version, if a feature is limited to Nightly.
// Once this feature lands in Beta/RC we should remove the wrapper.
activityTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.NIGHTLY,
) {
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
clickLinkMatchingText(pdfFileName)
verifyUrl(pdfFileURL)
verifyPageContent(pdfFileContent)
}
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
clickLinkMatchingText(pdfFileName)
verifyUrl(pdfFileURL)
verifyPageContent(pdfFileContent)
}
}
@Ignore("Failing because of https://bugzilla.mozilla.org/show_bug.cgi?id=1810132")
@SmokeTest
@Test
fun saveAndOpenPdfTest() {
runWithCondition(
// Returns the GeckoView channel set for the current version, if a feature is limited to Nightly.
// Once this feature lands in Beta/RC we should remove the wrapper.
activityTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.NIGHTLY,
) {
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
clickLinkMatchingText(pdfFileName)
verifyPageContent(pdfFileContent)
}.openThreeDotMenu {
}.clickShareButton {
}.clickSaveAsPDF {
// change back to simple filename
verifyDownloadPrompt(pdfFileName)
}.clickDownload {
}.clickOpen("application/pdf") {
assertExternalAppOpens("com.google.android.apps.docs")
}
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
clickLinkMatchingText(pdfFileName)
verifyPageContent(pdfFileContent)
}.openThreeDotMenu {
}.clickShareButton {
}.clickSaveAsPDF {
verifyDownloadPrompt(pdfFileName)
}.clickDownload {
}.clickOpen("application/pdf") {
assertExternalAppOpens("com.google.android.apps.docs")
}
}
}

@ -6,9 +6,7 @@ package org.mozilla.fenix.ui
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
@ -20,8 +18,6 @@ import org.mozilla.fenix.helpers.Constants.POCKET_RECOMMENDED_STORIES_UTM_PARAM
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.RetryTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
@ -97,33 +93,9 @@ class HomeScreenTest {
homeScreen { }.togglePrivateBrowsingMode()
homeScreen {
verifyHomeScreen()
verifyNavigationToolbar()
verifyHomePrivateBrowsingButton()
verifyHomeMenuButton()
verifyHomeWordmark()
verifyTabButton()
verifyPrivateSessionMessage()
verifyNavigationToolbar()
verifyHomeComponent()
verifyPrivateBrowsingHomeScreen()
}.openCommonMythsLink {
verifyUrl("common-myths-about-private-browsing")
mDevice.pressBack()
}
homeScreen {
// To deal with the race condition where multiple "add tab" buttons are present,
// we need to wait until previous HomeFragment View objects are gone.
mDevice.waitNotNull(Until.gone(By.text(privateSessionMessage)), waitingTime)
verifyHomeScreen()
verifyNavigationToolbar()
verifyHomePrivateBrowsingButton()
verifyHomeMenuButton()
verifyHomeWordmark()
verifyTabButton()
verifyPrivateSessionMessage()
verifyNavigationToolbar()
verifyHomeComponent()
}
}
@ -134,27 +106,39 @@ class HomeScreenTest {
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
verifyPageContent(firstWebPage.content)
verifyUrl(firstWebPage.url.toString())
}.goToHomescreen {
verifyJumpBackInSectionIsDisplayed()
verifyJumpBackInItemTitle(firstWebPage.title)
verifyJumpBackInItemWithUrl(firstWebPage.url.toString())
verifyJumpBackInItemTitle(activityTestRule, firstWebPage.title)
verifyJumpBackInItemWithUrl(activityTestRule, firstWebPage.url.toString())
verifyJumpBackInShowAllButton()
}.clickJumpBackInShowAllButton {
verifyExistingOpenTabs(firstWebPage.title)
}.closeTabDrawer {
}
homeScreen {
}.clickJumpBackInItemWithTitle(firstWebPage.title) {
verifyUrl(firstWebPage.url.toString())
clickLinkMatchingText("Link 1")
navigationToolbar {
}.enterURLAndEnterToBrowser(secondWebPage.url) {
verifyPageContent(secondWebPage.content)
verifyUrl(secondWebPage.url.toString())
}.goToHomescreen {
verifyJumpBackInSectionIsDisplayed()
verifyJumpBackInItemTitle(secondWebPage.title)
verifyJumpBackInItemWithUrl(secondWebPage.url.toString())
verifyJumpBackInItemTitle(activityTestRule, secondWebPage.title)
verifyJumpBackInItemWithUrl(activityTestRule, secondWebPage.url.toString())
}.openTabDrawer {
closeTabWithTitle(secondWebPage.title)
}.closeTabDrawer {
}
homeScreen {
verifyJumpBackInSectionIsDisplayed()
verifyJumpBackInItemTitle(activityTestRule, firstWebPage.title)
verifyJumpBackInItemWithUrl(activityTestRule, firstWebPage.url.toString())
}.openTabDrawer {
closeTab()
}
homeScreen {
verifyJumpBackInSectionIsNotDisplayed()
}
@ -227,7 +211,6 @@ class HomeScreenTest {
}
}
@Ignore("failing after a design refactor, see https://github.com/mozilla-mobile/fenix/issues/28472")
@Test
fun selectStoriesByTopicItemTest() {
activityTestRule.activityRule.applySettingsExceptions {

@ -1,13 +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.ui
import android.os.Build
import android.view.autofill.AutofillManager
import androidx.core.net.toUri
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
@ -16,22 +19,28 @@ 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.mDevice
import org.mozilla.fenix.helpers.TestHelper.restartApp
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
/**
* Tests for verifying:
* - the Logins and Passwords menu and sub-menus.
* - save login prompts.
* - saving logins based on the user's preferences.
*/
class LoginsTest {
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer
@get:Rule
val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
val activityTestRule =
HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
@Before
fun setUp() {
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
@ -49,6 +58,65 @@ class LoginsTest {
mockWebServer.shutdown()
}
// Tests the Logins and passwords menu items and default values
@Test
fun loginsAndPasswordsSettingsItemsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
// Necessary to scroll a little bit for all screen sizes
scrollToElementByText("Logins and passwords")
}.openLoginsAndPasswordSubMenu {
verifyDefaultView()
verifyAutofillInFirefoxToggle(true)
verifyAutofillLoginsInOtherAppsToggle(false)
}
}
// Tests only for initial state without signing in.
// For tests after signing in, see SyncIntegration test suite
@Test
fun savedLoginsMenuItemsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
// Necessary to scroll a little bit for all screen sizes
scrollToElementByText("Logins and passwords")
}.openLoginsAndPasswordSubMenu {
verifyDefaultView()
}.openSavedLogins {
verifySecurityPromptForLogins()
tapSetupLater()
// Verify that logins list is empty
verifyEmptySavedLoginsListView()
}
}
@Test
fun syncLoginsMenuItemsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
// Necessary to scroll a little bit for all screen sizes
scrollToElementByText("Logins and passwords")
}.openLoginsAndPasswordSubMenu {
}.openSyncLogins {
verifyReadyToScanOption()
verifyUseEmailOption()
}
}
@Test
fun saveLoginsAndPasswordsOptionsItemsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openLoginsAndPasswordSubMenu {
}.openSaveLoginsAndPasswordsOptions {
verifySaveLoginsOptionsView()
}
}
@Test
fun saveLoginFromPromptTest() {
val saveLoginTest =
@ -63,7 +131,7 @@ class LoginsTest {
browserScreen {
}.openThreeDotMenu {
}.openSettings {
TestHelper.scrollToElementByText("Logins and passwords")
scrollToElementByText("Logins and passwords")
}.openLoginsAndPasswordSubMenu {
verifyDefaultView()
}.openSavedLogins {
@ -86,6 +154,7 @@ class LoginsTest {
}.enterURLAndEnterToBrowser(loginPage.toUri()) {
fillAndSubmitLoginCredentials(userName, password)
saveLoginFromPrompt("Save")
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openSettings {
}.openLoginsAndPasswordSubMenu {
@ -107,6 +176,7 @@ class LoginsTest {
clickSubmitLoginButton()
// Don't save the login, add to exceptions
saveLoginFromPrompt("Never save")
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openSettings {
}.openLoginsAndPasswordSubMenu {
@ -115,6 +185,7 @@ class LoginsTest {
verifySecurityPromptForLogins()
tapSetupLater()
// Verify that the login list is empty
verifyEmptySavedLoginsListView()
verifyNotSavedLoginFromPrompt()
}.goBack {
}.openLoginExceptions {
@ -137,13 +208,14 @@ class LoginsTest {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(saveLoginTest.url) {
enterPassword("test")
mDevice.waitForIdle()
clickSubmitLoginButton()
verifyUpdateLoginPromptIsShown()
// Click Update to change the saved password
saveLoginFromPrompt("Update")
}.openThreeDotMenu {
}.openSettings {
TestHelper.scrollToElementByText("Logins and passwords")
scrollToElementByText("Logins and passwords")
}.openLoginsAndPasswordSubMenu {
}.openSavedLogins {
verifySecurityPromptForLogins()
@ -186,6 +258,7 @@ class LoginsTest {
}.enterURLAndEnterToBrowser(loginPage.toUri()) {
fillAndSubmitLoginCredentials("mozilla", "firefox")
saveLoginFromPrompt("Save")
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openSettings {
}.openLoginsAndPasswordSubMenu {
@ -210,6 +283,7 @@ class LoginsTest {
}.enterURLAndEnterToBrowser(loginPage.toUri()) {
fillAndSubmitLoginCredentials("mozilla", "firefox")
saveLoginFromPrompt("Save")
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openSettings {
}.openLoginsAndPasswordSubMenu {
@ -243,6 +317,7 @@ class LoginsTest {
}.enterURLAndEnterToBrowser(loginPage.toUri()) {
fillAndSubmitLoginCredentials("mozilla", "firefox")
saveLoginFromPrompt("Save")
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openSettings {
}.openLoginsAndPasswordSubMenu {
@ -253,6 +328,7 @@ class LoginsTest {
clickEditLoginButton()
clickClearUserNameButton()
saveEditedLogin()
verifyLoginItemUsername("")
}
}
@ -265,6 +341,7 @@ class LoginsTest {
}.enterURLAndEnterToBrowser(loginPage.toUri()) {
fillAndSubmitLoginCredentials("mozilla", "firefox")
saveLoginFromPrompt("Save")
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openSettings {
}.openLoginsAndPasswordSubMenu {
@ -290,6 +367,7 @@ class LoginsTest {
}.enterURLAndEnterToBrowser(loginPage.toUri()) {
fillAndSubmitLoginCredentials("mozilla", "firefox")
saveLoginFromPrompt("Save")
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openSettings {
}.openLoginsAndPasswordSubMenu {
@ -315,6 +393,7 @@ class LoginsTest {
}.enterURLAndEnterToBrowser(loginPage.url) {
clickSubmitLoginButton()
saveLoginFromPrompt("Save")
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openSettings {
}.openLoginsAndPasswordSubMenu {
@ -331,7 +410,7 @@ class LoginsTest {
clickDeleteLoginButton()
verifyLoginDeletionPrompt()
clickConfirmDeleteLogin()
// The account remains displayed, see: https://github.com/mozilla-mobile/fenix/issues/23212
// The account remains displayed, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1812431
// verifyNotSavedLoginFromPrompt()
}
}
@ -381,9 +460,9 @@ class LoginsTest {
}.openThreeDotMenu {
}.openSettings {
}.openLoginsAndPasswordSubMenu {
verifyAutofillToggle(true)
clickAutofillOption()
verifyAutofillToggle(false)
verifyAutofillInFirefoxToggle(true)
clickAutofillInFirefoxOption()
verifyAutofillInFirefoxToggle(false)
}.goBack {
}
@ -395,6 +474,7 @@ class LoginsTest {
}
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1812995")
@Test
fun verifyLoginIsNotUpdatedTest() {
val loginPage = "https://mozilla-mobile.github.io/testapp/v2.0/loginForm.html"
@ -521,6 +601,7 @@ class LoginsTest {
}.enterURLAndEnterToBrowser(secondLoginPage.toUri()) {
fillAndSubmitLoginCredentials("mozilla", "firefox")
saveLoginFromPrompt("Save")
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openSettings {
}.openLoginsAndPasswordSubMenu {

@ -34,6 +34,11 @@ import java.util.Locale
class NavigationToolbarTest {
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer
private val downloadTestPage =
"https://storage.googleapis.com/mobile_test_assets/test_app/downloads.html"
private val pdfFileName = "washington.pdf"
private val pdfFileURL = "storage.googleapis.com/mobile_test_assets/public/washington.pdf"
private val pdfFileContent = "Washington Crossing the Delaware"
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
@get:Rule
@ -174,9 +179,46 @@ class NavigationToolbarTest {
verifyFindPrevInPageResult("2/3")
clickFindInPagePrevButton()
verifyFindPrevInPageResult("1/3")
}.closeFindInPageWithCloseButton {
verifyFindInPageBar(false)
}.openThreeDotMenu {
}.openFindInPage {
enterFindInPageQuery("3")
verifyFindNextInPageResult("1/1")
}.closeFindInPage { }
}.closeFindInPageWithBackButton {
verifyFindInPageBar(false)
}
}
@Test
fun pdfFindInPageTest() {
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
clickLinkMatchingText(pdfFileName)
verifyUrl(pdfFileURL)
verifyPageContent(pdfFileContent)
}.openThreeDotMenu {
verifyThreeDotMenuExists()
verifyFindInPageButton()
}.openFindInPage {
verifyFindInPageNextButton()
verifyFindInPagePrevButton()
verifyFindInPageCloseButton()
enterFindInPageQuery("o")
verifyFindNextInPageResult("1/2")
clickFindInPageNextButton()
verifyFindNextInPageResult("2/2")
clickFindInPagePrevButton()
verifyFindPrevInPageResult("1/2")
}.closeFindInPageWithCloseButton {
verifyFindInPageBar(false)
}.openThreeDotMenu {
}.openFindInPage {
enterFindInPageQuery("l")
verifyFindNextInPageResult("1/1")
}.closeFindInPageWithBackButton {
verifyFindInPageBar(false)
}
}
@Test

@ -0,0 +1,119 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.ui
import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.gleanplumb.CustomAttributeProvider
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.nimbus.Messaging
/**
* This test is to test the integrity of messages hardcoded in the FML.
*
* It tests if the trigger expressions are valid, all the fields are complete
* and a simple check if they are localized (don't contain `_`).
*/
class NimbusMessagingMessageTest {
private lateinit var feature: Messaging
private lateinit var mDevice: UiDevice
private lateinit var context: Context
private val storage
get() = context.components.analytics.messagingStorage
@get:Rule
val activityTestRule =
HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
@Before
fun setUp() {
context = TestHelper.appContext
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
feature = FxNimbus.features.messaging.value()
}
/**
* Check if all messages in the FML are internally consistent with the
* rest of the FML. This check is done in the `NimbusMessagingStorage`
* class.
*/
@Test
fun testAllMessageIntegrity() = runTest {
val messages = storage.getMessages()
val rawMessages = feature.messages
assertTrue(rawMessages.isNotEmpty())
if (messages.size != rawMessages.size) {
val expected = rawMessages.keys.toHashSet()
val observed = messages.map { it.id }.toHashSet()
val missing = expected - observed
fail("Problem with message(s) in FML: $missing")
}
assertEquals(messages.size, rawMessages.size)
}
/**
* Check if the messages' triggers are well formed JEXL.
*/
@Test
fun testAllMessageTriggers() = runTest {
val nimbus = context.components.analytics.experiments
val helper = nimbus.createMessageHelper(
CustomAttributeProvider.getCustomAttributes(context),
)
val messages = storage.getMessages()
messages.forEach { message ->
storage.isMessageEligible(message, helper)
if (storage.malFormedMap.isNotEmpty()) {
fail("${message.id} has a problem with its JEXL trigger: ${storage.malFormedMap.keys}")
}
}
}
private fun checkIsLocalized(string: String) {
assertFalse(string.isBlank())
// The check will almost always succeed, since the generated code
// will not compile if this is true, and there is no resource available.
assertFalse(string.matches(Regex("[a-z][_a-z\\d]*")))
}
/**
* Check that the messages are localized.
*/
@Test
fun testAllMessagesAreLocalized() {
feature.messages.values.forEach { message ->
message.buttonLabel?.let(::checkIsLocalized)
message.title?.let(::checkIsLocalized)
checkIsLocalized(message.text)
}
}
@Test
fun testIndividualMessagesAreValid() {
val expectedMessages = listOf(
"default-browser",
"default-browser-notification",
)
val rawMessages = feature.messages
for (id in expectedMessages) {
assertTrue(rawMessages.containsKey(id))
}
}
}

@ -65,7 +65,11 @@ class OnboardingTest {
)
verifyToolbarPlacementCard(isBottomChecked = true, isTopChecked = false)
verifySignInToSyncCard()
verifyPrivacyProtectionCard(isStandardChecked = true, isStrictChecked = false)
verifyPrivacyProtectionCard(
settings = activityTestRule.activity.getSettings(),
isStandardChecked = true,
isStrictChecked = false,
)
verifyPrivacyNoticeCard()
verifyStartBrowsingSection()
verifyNavigationToolbarItems("0")
@ -194,11 +198,23 @@ class OnboardingTest {
@Test
fun privacyProtectionByDefaultCardTest() {
homeScreen {
verifyPrivacyProtectionCard(isStandardChecked = true, isStrictChecked = false)
verifyPrivacyProtectionCard(
settings = activityTestRule.activity.getSettings(),
isStandardChecked = true,
isStrictChecked = false,
)
clickStrictTrackingProtectionButton()
verifyPrivacyProtectionCard(isStandardChecked = false, isStrictChecked = true)
verifyPrivacyProtectionCard(
settings = activityTestRule.activity.getSettings(),
isStandardChecked = false,
isStrictChecked = true,
)
clickStandardTrackingProtectionButton()
verifyPrivacyProtectionCard(isStandardChecked = true, isStrictChecked = false)
verifyPrivacyProtectionCard(
settings = activityTestRule.activity.getSettings(),
isStandardChecked = true,
isStrictChecked = false,
)
}
}
@ -217,8 +233,8 @@ class OnboardingTest {
homeScreen {
verifyPrivacyNoticeCard()
}.clickPrivacyNoticeButton {
verifyUrl(privacyNoticeLink)
}.goBack {
verifyCustomTabToolbarTitle("Firefox Privacy Notice")
}.goBackToOnboardingScreen {
verifyPrivacyNoticeCard()
}
}

@ -8,7 +8,11 @@ import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.Constants.PackageName.GMAIL_APP
import org.mozilla.fenix.helpers.Constants.PackageName.PHONE_APP
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.assertNativeAppOpens
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.ui.robots.addToHomeScreen
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.customTabScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.pwaScreen
@ -98,4 +102,40 @@ class PwaTest {
verifyPwaActivityInCurrentTask()
}
}
@SmokeTest
@Test
fun saveLoginsInPWATest() {
val pwaPage = "https://mozilla-mobile.github.io/testapp/loginForm"
val shortcutTitle = "TEST_APP"
navigationToolbar {
}.enterURLAndEnterToBrowser(pwaPage.toUri()) {
verifyNotificationDotOnMainMenu()
}.openThreeDotMenu {
}.clickInstall {
clickAddAutomaticallyButton()
}.openHomeScreenShortcut(shortcutTitle) {
mDevice.waitForIdle()
fillAndSubmitLoginCredentials("mozilla", "firefox")
verifySaveLoginPromptIsDisplayed()
saveLoginFromPrompt("Save")
TestHelper.openAppFromExternalLink(pwaPage)
browserScreen {
}.openThreeDotMenu {
}.openSettings {
}.openLoginsAndPasswordSubMenu {
}.openSavedLogins {
verifySecurityPromptForLogins()
tapSetupLater()
verifySavedLoginsSectionUsername("mozilla")
}
addToHomeScreen {
}.searchAndOpenHomeScreenShortcut(shortcutTitle) {
verifyPrefilledPWALoginCredentials("mozilla", shortcutTitle)
}
}
}
}

@ -16,6 +16,7 @@ import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.ui.robots.clickAlwaysButton
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
@ -61,7 +62,7 @@ class SettingsAdvancedTest {
verifyAdvancedHeading()
verifyAddons()
verifyOpenLinksInAppsButton()
verifyOpenLinksInAppsSwitchState(false)
verifyOpenLinksInAppsState("Never")
verifyRemoteDebug()
verifyLeakCanaryButton()
}
@ -76,9 +77,11 @@ class SettingsAdvancedTest {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsSwitchState(false)
clickOpenLinksInAppsSwitch()
verifyOpenLinksInAppsSwitchState(true)
verifyOpenLinksInAppsButton()
verifyOpenLinksInAppsState("Never")
}.openOpenLinksInAppsMenu {
clickAlwaysButton()
}.goBack {
}.goBack {}
navigationToolbar {

@ -16,26 +16,18 @@ import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.getLoremIpsumAsset
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeLong
import org.mozilla.fenix.helpers.TestHelper.getStringResource
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.registerAndCleanupIdlingResources
import org.mozilla.fenix.helpers.TestHelper.runWithSystemLocaleChanged
import org.mozilla.fenix.ui.SettingsBasicsTest.CreditCard.MOCK_CREDIT_CARD_NUMBER
import org.mozilla.fenix.ui.SettingsBasicsTest.CreditCard.MOCK_EXPIRATION_MONTH
import org.mozilla.fenix.ui.SettingsBasicsTest.CreditCard.MOCK_EXPIRATION_YEAR
import org.mozilla.fenix.ui.SettingsBasicsTest.CreditCard.MOCK_LAST_CARD_DIGITS
import org.mozilla.fenix.ui.SettingsBasicsTest.CreditCard.MOCK_NAME_ON_CARD
import org.mozilla.fenix.ui.robots.checkTextSizeOnWebsite
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.util.FRENCH_LANGUAGE_HEADER
import org.mozilla.fenix.ui.util.FRENCH_SYSTEM_LOCALE_OPTION
import org.mozilla.fenix.ui.util.FR_SETTINGS
import org.mozilla.fenix.ui.util.ROMANIAN_LANGUAGE_HEADER
import java.time.LocalDate
import java.util.Locale
/**
@ -46,14 +38,6 @@ class SettingsBasicsTest {
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
private lateinit var mockWebServer: MockWebServer
object CreditCard {
const val MOCK_CREDIT_CARD_NUMBER = "5555555555554444"
const val MOCK_LAST_CARD_DIGITS = "4444"
const val MOCK_NAME_ON_CARD = "Mastercard"
const val MOCK_EXPIRATION_MONTH = "February"
val MOCK_EXPIRATION_YEAR = (LocalDate.now().year + 1).toString()
}
@get:Rule
val activityIntentTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
@ -119,51 +103,6 @@ class SettingsBasicsTest {
}
}
@SmokeTest
@Test
fun verifyCreditCardAutofillTest() {
val creditCardFormPage = TestAssetHelper.getCreditCardFormAsset(mockWebServer)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openAutofillSubMenu {
clickAddCreditCardButton()
fillAndSaveCreditCard(MOCK_CREDIT_CARD_NUMBER, MOCK_NAME_ON_CARD, MOCK_EXPIRATION_MONTH, MOCK_EXPIRATION_YEAR)
// Opening Manage saved cards to dismiss here the Secure your credit prompt
clickManageSavedCardsButton()
clickSecuredCreditCardsLaterButton()
}.goBackToAutofillSettings {
}.goBack {
}.goBack {
}
navigationToolbar {
}.enterURLAndEnterToBrowser(creditCardFormPage.url) {
clickCardNumberTextBox()
clickSelectCreditCardButton()
clickCreditCardSuggestion(MOCK_LAST_CARD_DIGITS)
verifyAutofilledCreditCard(MOCK_CREDIT_CARD_NUMBER)
}
}
@SmokeTest
@Test
fun deleteSavedCreditCardTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openAutofillSubMenu {
clickAddCreditCardButton()
fillAndSaveCreditCard(MOCK_CREDIT_CARD_NUMBER, MOCK_NAME_ON_CARD, MOCK_EXPIRATION_MONTH, MOCK_EXPIRATION_YEAR)
clickManageSavedCardsButton()
clickSecuredCreditCardsLaterButton()
clickSavedCreditCard()
clickDeleteCreditCardButton()
clickConfirmDeleteCreditCardButton()
verifyAddCreditCardsButton()
}
}
@SmokeTest
@Test
fun switchLanguageTest() {

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

@ -0,0 +1,196 @@
/* 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.core.net.toUri
import androidx.test.espresso.Espresso.pressBack
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestHelper.exitMenu
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
class SettingsHTTPSOnlyModeTest {
private val httpPageUrl = "http://example.com/"
private val httpsPageUrl = "https://example.com/"
private val insecureHttpPage = "http.badssl.com"
// "HTTPs not supported" error page contents:
private val httpsOnlyErrorTitle = "Secure site not available"
private val httpsOnlyErrorMessage = "Most likely, the website simply does not support HTTPS."
private val httpsOnlyErrorMessage2 = "However, its also possible that an attacker is involved. If you continue to the website, you should not enter any sensitive info. If you continue, HTTPS-Only mode will be turned off temporarily for the site."
private val httpsOnlyContinueButton = "Continue to HTTP Site"
private val httpsOnlyBackButton = "Go Back (Recommended)"
@get:Rule
val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
@Test
fun httpsOnlyModeMenuItemsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openHttpsOnlyModeMenu {
verifyHttpsOnlyModeMenuHeader()
verifyHttpsOnlyModeSummary()
verifyHttpsOnlyModeIsEnabled(false)
verifyHttpsOnlyModeOptionsEnabled(false)
verifyHttpsOnlyOptionSelected(
allTabsOptionSelected = false,
privateTabsOptionSelected = false,
)
clickHttpsOnlyModeSwitch()
verifyHttpsOnlyModeIsEnabled(true)
verifyHttpsOnlyModeOptionsEnabled(true)
verifyHttpsOnlyOptionSelected(
allTabsOptionSelected = true,
privateTabsOptionSelected = false,
)
}.goBack {
verifySettingsToolbar()
}
}
@SmokeTest
@Test
fun httpsOnlyModeEnabledInNormalBrowsingTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openHttpsOnlyModeMenu {
clickHttpsOnlyModeSwitch()
verifyHttpsOnlyOptionSelected(
allTabsOptionSelected = true,
privateTabsOptionSelected = false,
)
exitMenu()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(httpPageUrl.toUri()) {
waitForPageToLoad()
}.openNavigationToolbar {
verifyUrl(httpsPageUrl)
}.enterURLAndEnterToBrowser(insecureHttpPage.toUri()) {
verifyPageContent(httpsOnlyErrorTitle)
verifyPageContent(httpsOnlyErrorMessage)
verifyPageContent(httpsOnlyErrorMessage2)
verifyPageContent(httpsOnlyBackButton)
clickLinkMatchingText(httpsOnlyBackButton)
verifyPageContent("Example Domain")
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(insecureHttpPage.toUri()) {
clickLinkMatchingText(httpsOnlyContinueButton)
verifyPageContent("http.badssl.com")
}
}
@Test
fun httpsOnlyModeExceptionPersistsForCurrentSession() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openHttpsOnlyModeMenu {
clickHttpsOnlyModeSwitch()
verifyHttpsOnlyOptionSelected(
allTabsOptionSelected = true,
privateTabsOptionSelected = false,
)
exitMenu()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(insecureHttpPage.toUri()) {
verifyPageContent(httpsOnlyErrorTitle)
clickLinkMatchingText(httpsOnlyContinueButton)
verifyPageContent("http.badssl.com")
}.openTabDrawer {
closeTab()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(insecureHttpPage.toUri()) {
verifyPageContent("http.badssl.com")
}
}
@Test
fun httpsOnlyModeEnabledOnlyInPrivateBrowsingTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openHttpsOnlyModeMenu {
clickHttpsOnlyModeSwitch()
selectHttpsOnlyModeOption(
allTabsOptionSelected = false,
privateTabsOptionSelected = true,
)
exitMenu()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(insecureHttpPage.toUri()) {
verifyPageContent("http.badssl.com")
}.goToHomescreen {
}.togglePrivateBrowsingMode()
navigationToolbar {
}.enterURLAndEnterToBrowser(httpPageUrl.toUri()) {
waitForPageToLoad()
}.openNavigationToolbar {
verifyUrl(httpsPageUrl)
}.enterURLAndEnterToBrowser(insecureHttpPage.toUri()) {
verifyPageContent(httpsOnlyErrorTitle)
verifyPageContent(httpsOnlyErrorMessage)
verifyPageContent(httpsOnlyErrorMessage2)
verifyPageContent(httpsOnlyBackButton)
clickLinkMatchingText(httpsOnlyBackButton)
verifyPageContent("Example Domain")
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(insecureHttpPage.toUri()) {
clickLinkMatchingText(httpsOnlyContinueButton)
verifyPageContent("http.badssl.com")
}
}
@Test
fun turnOffHttpsOnlyModeTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openHttpsOnlyModeMenu {
clickHttpsOnlyModeSwitch()
verifyHttpsOnlyOptionSelected(
allTabsOptionSelected = true,
privateTabsOptionSelected = false,
)
exitMenu()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(httpPageUrl.toUri()) {
waitForPageToLoad()
}.openNavigationToolbar {
verifyUrl(httpsPageUrl)
pressBack()
}
browserScreen {
}.openTabDrawer {
closeTab()
}
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openHttpsOnlyModeMenu {
clickHttpsOnlyModeSwitch()
verifyHttpsOnlyModeIsEnabled(false)
exitMenu()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(httpPageUrl.toUri()) {
waitForPageToLoad()
}.openNavigationToolbar {
verifyUrl(httpPageUrl)
pressBack()
}
}
}

@ -1,728 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.ui
import android.os.Build
import android.view.autofill.AutofillManager
import androidx.core.net.toUri
import androidx.test.filters.SdkSuppress
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.R
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.getStorageTestAsset
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.appContext
import org.mozilla.fenix.helpers.TestHelper.exitMenu
import org.mozilla.fenix.helpers.TestHelper.generateRandomString
import org.mozilla.fenix.helpers.TestHelper.getStringResource
import org.mozilla.fenix.helpers.TestHelper.openAppFromExternalLink
import org.mozilla.fenix.helpers.TestHelper.restartApp
import org.mozilla.fenix.helpers.TestHelper.setNetworkEnabled
import org.mozilla.fenix.ui.robots.addToHomeScreen
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.settingsScreen
/**
* Tests for verifying the main three dot menu options
*
*/
class SettingsPrivacyTest {
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer
private val pageShortcutName = generateRandomString(5)
@get:Rule
val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
@Before
fun setUp() {
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
val autofillManager: AutofillManager =
appContext.getSystemService(AutofillManager::class.java)
autofillManager.disableAutofillServices()
}
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
// Walks through settings privacy menu and sub-menus to ensure all items are present
@Test
fun settingsPrivacyItemsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
// PRIVACY
verifyPrivacyHeading()
// PRIVATE BROWSING
verifyPrivateBrowsingButton()
}.openPrivateBrowsingSubMenu {
verifyNavigationToolBarHeader()
}.goBack {
// HTTPS-Only Mode
verifyHTTPSOnlyModeButton()
verifyHTTPSOnlyModeState("Off")
// ENHANCED TRACKING PROTECTION
verifyEnhancedTrackingProtectionButton()
verifyEnhancedTrackingProtectionState("Standard")
}.openEnhancedTrackingProtectionSubMenu {
verifyNavigationToolBarHeader()
verifyEnhancedTrackingProtectionProtectionSubMenuItems()
// ENHANCED TRACKING PROTECTION EXCEPTION
}.openExceptions {
verifyNavigationToolBarHeader()
verifyEnhancedTrackingProtectionProtectionExceptionsSubMenuItems()
}.goBack {
}.goBack {
// SITE PERMISSIONS
verifySitePermissionsButton()
}.openSettingsSubMenuSitePermissions {
verifyNavigationToolBarHeader()
verifySitePermissionsSubMenuItems()
// SITE PERMISSIONS AUTOPLAY
}.openAutoPlay {
verifyNavigationToolBarHeader("Autoplay")
verifySitePermissionsAutoPlaySubMenuItems()
}.goBack {
// SITE PERMISSIONS CAMERA
}.openCamera {
verifyNavigationToolBarHeader("Camera")
verifySitePermissionsCommonSubMenuItems()
verifyToggleNameToON("3. Toggle Camera to ON")
}.goBack {
// SITE PERMISSIONS LOCATION
}.openLocation {
verifyNavigationToolBarHeader("Location")
verifySitePermissionsCommonSubMenuItems()
verifyToggleNameToON("3. Toggle Location to ON")
}.goBack {
// SITE PERMISSIONS MICROPHONE
}.openMicrophone {
verifyNavigationToolBarHeader("Microphone")
verifySitePermissionsCommonSubMenuItems()
verifyToggleNameToON("3. Toggle Microphone to ON")
}.goBack {
// SITE PERMISSIONS NOTIFICATION
}.openNotification {
verifyNavigationToolBarHeader("Notification")
verifySitePermissionsNotificationSubMenuItems()
}.goBack {
// SITE PERMISSIONS PERSISTENT STORAGE
}.openPersistentStorage {
verifyNavigationToolBarHeader("Persistent Storage")
verifySitePermissionsPersistentStorageSubMenuItems()
}.goBack {
// SITE PERMISSIONS EXCEPTIONS
}.openExceptions {
verifyNavigationToolBarHeader()
verifySitePermissionsExceptionSubMenuItems()
}.goBack {
}.goBack {
// DELETE BROWSING DATA
verifyDeleteBrowsingDataButton()
}.openSettingsSubMenuDeleteBrowsingData {
verifyNavigationToolBarHeader()
verifyDeleteBrowsingDataSubMenuItems()
}.goBack {
// DELETE BROWSING DATA ON QUIT
verifyDeleteBrowsingDataOnQuitButton()
verifyDeleteBrowsingDataOnQuitState("Off")
}.openSettingsSubMenuDeleteBrowsingDataOnQuit {
verifyNavigationToolBarHeader()
verifyDeleteBrowsingDataOnQuitSubMenuItems()
}.goBack {
// NOTIFICATIONS
verifyNotificationsButton()
}.openSettingsSubMenuNotifications {
verifySystemNotificationsView()
}.goBack {
// DATA COLLECTION
verifyDataCollectionButton()
}.openSettingsSubMenuDataCollection {
verifyNavigationToolBarHeader()
verifyDataCollectionSubMenuItems()
}.goBack {
}.goBack {
verifyHomeComponent()
}
}
// Tests only for initial state without signing in.
// For tests after singing in, see SyncIntegration test suite
@Test
fun loginsAndPasswordsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
// Necessary to scroll a little bit for all screen sizes
TestHelper.scrollToElementByText("Logins and passwords")
}.openLoginsAndPasswordSubMenu {
verifyDefaultView()
verifyDefaultValueAutofillLogins(InstrumentationRegistry.getInstrumentation().targetContext)
verifyDefaultValueExceptions()
}.openSavedLogins {
verifySecurityPromptForLogins()
tapSetupLater()
// Verify that logins list is empty
// Issue #7272 nothing is shown
}.goBack {
}.openSyncLogins {
verifyReadyToScanOption()
verifyUseEmailOption()
}
}
@Test
fun saveLoginsAndPasswordsOptions() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openLoginsAndPasswordSubMenu {
}.openSaveLoginsAndPasswordsOptions {
verifySaveLoginsOptionsView()
}
}
@Test
fun verifyPrivateBrowsingMenuItemsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openPrivateBrowsingSubMenu {
verifyAddPrivateBrowsingShortcutButton()
verifyOpenLinksInPrivateTab()
verifyOpenLinksInPrivateTabOff()
}.goBack {
verifySettingsView()
}
}
@Test
fun openExternalLinksInPrivateTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
setOpenLinksInPrivateOn()
openAppFromExternalLink(firstWebPage.url.toString())
browserScreen {
verifyUrl(firstWebPage.url.toString())
}.openTabDrawer {
verifyPrivateModeSelected()
}.closeTabDrawer {
}.goToHomescreen { }
setOpenLinksInPrivateOff()
// We need to open a different link, otherwise it will open the same session
openAppFromExternalLink(secondWebPage.url.toString())
browserScreen {
verifyUrl(secondWebPage.url.toString())
}.openTabDrawer {
verifyNormalModeSelected()
}
}
@Test
fun launchPageShortcutInPrivateModeTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
setOpenLinksInPrivateOn()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
}.openAddToHomeScreen {
addShortcutName(pageShortcutName)
clickAddShortcutButton()
clickAddAutomaticallyButton()
verifyShortcutAdded(pageShortcutName)
}
mDevice.waitForIdle()
// We need to close the existing tab here, to open a different session
restartApp(activityTestRule)
browserScreen {
}.openTabDrawer {
closeTab()
}
addToHomeScreen {
}.searchAndOpenHomeScreenShortcut(pageShortcutName) {
}.openTabDrawer {
verifyPrivateModeSelected()
}
}
@Test
fun launchLinksInPrivateToggleOffStateDoesntChangeTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
setOpenLinksInPrivateOn()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
expandMenu()
}.openAddToHomeScreen {
addShortcutName(pageShortcutName)
clickAddShortcutButton()
clickAddAutomaticallyButton()
}.openHomeScreenShortcut(pageShortcutName) {
}.goToHomescreen { }
setOpenLinksInPrivateOff()
restartApp(activityTestRule)
mDevice.waitForIdle()
addToHomeScreen {
}.searchAndOpenHomeScreenShortcut(pageShortcutName) {
}.openTabDrawer {
verifyNormalModeSelected()
}.closeTabDrawer {
}.openThreeDotMenu {
}.openSettings {
}.openPrivateBrowsingSubMenu {
verifyOpenLinksInPrivateTabOff()
}
}
@Test
fun addPrivateBrowsingShortcut() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openPrivateBrowsingSubMenu {
cancelPrivateShortcutAddition()
addPrivateShortcutToHomescreen()
verifyPrivateBrowsingShortcutIcon()
}.openPrivateBrowsingShortcut {
verifySearchView()
}.openBrowser {
}.openTabDrawer {
verifyPrivateModeSelected()
}
}
// Verifies that you can go to System settings and change app's permissions from inside the app
@SmokeTest
@Test
@SdkSuppress(minSdkVersion = 29)
fun redirectToAppPermissionsSystemSettingsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuSitePermissions {
}.openCamera {
verifyBlockedByAndroid()
}.goBack {
}.openLocation {
verifyBlockedByAndroid()
}.goBack {
}.openMicrophone {
verifyBlockedByAndroid()
clickGoToSettingsButton()
openAppSystemPermissionsSettings()
switchAppPermissionSystemSetting("Camera", "Allow")
goBackToSystemAppPermissionSettings()
verifySystemGrantedPermission("Camera")
switchAppPermissionSystemSetting("Location", "Allow")
goBackToSystemAppPermissionSettings()
verifySystemGrantedPermission("Location")
switchAppPermissionSystemSetting("Microphone", "Allow")
goBackToSystemAppPermissionSettings()
verifySystemGrantedPermission("Microphone")
goBackToPermissionsSettingsSubMenu()
verifyUnblockedByAndroid()
}.goBack {
}.openLocation {
verifyUnblockedByAndroid()
}.goBack {
}.openCamera {
verifyUnblockedByAndroid()
}
}
@Test
fun deleteBrowsingDataOptionStatesTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingData {
verifyAllCheckBoxesAreChecked()
switchBrowsingHistoryCheckBox()
switchCachedFilesCheckBox()
verifyOpenTabsCheckBox(true)
verifyBrowsingHistoryDetails(false)
verifyCookiesCheckBox(true)
verifyCachedFilesCheckBox(false)
verifySitePermissionsCheckBox(true)
verifyDownloadsCheckBox(true)
}
restartApp(activityTestRule)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingData {
verifyOpenTabsCheckBox(true)
verifyBrowsingHistoryDetails(false)
verifyCookiesCheckBox(true)
verifyCachedFilesCheckBox(false)
verifySitePermissionsCheckBox(true)
verifyDownloadsCheckBox(true)
switchOpenTabsCheckBox()
switchBrowsingHistoryCheckBox()
switchCookiesCheckBox()
switchCachedFilesCheckBox()
switchSitePermissionsCheckBox()
switchDownloadsCheckBox()
verifyOpenTabsCheckBox(false)
verifyBrowsingHistoryDetails(true)
verifyCookiesCheckBox(false)
verifyCachedFilesCheckBox(true)
verifySitePermissionsCheckBox(false)
verifyDownloadsCheckBox(false)
}
restartApp(activityTestRule)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingData {
verifyOpenTabsCheckBox(false)
verifyBrowsingHistoryDetails(true)
verifyCookiesCheckBox(false)
verifyCachedFilesCheckBox(true)
verifySitePermissionsCheckBox(false)
verifyDownloadsCheckBox(false)
}
}
@Test
fun deleteTabsDataWithNoOpenTabsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingData {
verifyAllCheckBoxesAreChecked()
selectOnlyOpenTabsCheckBox()
clickDeleteBrowsingDataButton()
confirmDeletionAndAssertSnackbar()
}
settingsScreen {
verifyGeneralHeading()
}
}
@SmokeTest
@Test
fun deleteTabsDataTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingData {
verifyAllCheckBoxesAreChecked()
selectOnlyOpenTabsCheckBox()
clickDeleteBrowsingDataButton()
clickDialogCancelButton()
verifyOpenTabsCheckBox(true)
clickDeleteBrowsingDataButton()
confirmDeletionAndAssertSnackbar()
}
settingsScreen {
verifyGeneralHeading()
}.openSettingsSubMenuDeleteBrowsingData {
verifyOpenTabsDetails("0")
}.goBack {
}.goBack {
}.openTabDrawer {
verifyNoOpenTabsInNormalBrowsing()
}
}
@SmokeTest
@Test
fun deleteBrowsingHistoryAndSiteDataTest() {
val storageWritePage = getStorageTestAsset(mockWebServer, "storage_write.html").url
val storageCheckPage = getStorageTestAsset(mockWebServer, "storage_check.html").url
navigationToolbar {
}.enterURLAndEnterToBrowser(storageWritePage) {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(storageCheckPage) {
verifyPageContent("Session storage has value")
verifyPageContent("Local storage has value")
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingData {
verifyBrowsingHistoryDetails("2")
selectOnlyBrowsingHistoryCheckBox()
clickDeleteBrowsingDataButton()
clickDialogCancelButton()
verifyBrowsingHistoryDetails(true)
clickDeleteBrowsingDataButton()
confirmDeletionAndAssertSnackbar()
verifyBrowsingHistoryDetails("0")
exitMenu()
}
navigationToolbar {
}.openThreeDotMenu {
}.openHistory {
verifyEmptyHistoryView()
mDevice.pressBack()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(storageCheckPage) {
verifyPageContent("Session storage empty")
verifyPageContent("Local storage empty")
}
}
@SmokeTest
@Test
fun deleteCookiesTest() {
val genericPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val cookiesTestPage = getStorageTestAsset(mockWebServer, "storage_write.html").url
// Browsing a generic page to allow GV to load on a fresh run
navigationToolbar {
}.enterURLAndEnterToBrowser(genericPage.url) {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(cookiesTestPage) {
verifyPageContent("No cookies set")
clickSetCookiesButton()
verifyPageContent("user=android")
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingData {
selectOnlyCookiesCheckBox()
clickDeleteBrowsingDataButton()
confirmDeletionAndAssertSnackbar()
exitMenu()
}
browserScreen {
}.openThreeDotMenu {
}.refreshPage {
verifyPageContent("No cookies set")
}
}
@SmokeTest
@Test
fun deleteCachedFilesTest() {
val pocketTopArticles = getStringResource(R.string.pocket_pinned_top_articles)
homeScreen {
verifyExistingTopSitesTabs(pocketTopArticles)
}.openTopSiteTabWithTitle(pocketTopArticles) {
waitForPageToLoad()
}.openTabDrawer {
}.openNewTab {
}.submitQuery("about:cache") {
// disabling wifi to prevent downloads in the background
setNetworkEnabled(enabled = false)
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingData {
selectOnlyCachedFilesCheckBox()
clickDeleteBrowsingDataButton()
confirmDeletionAndAssertSnackbar()
exitMenu()
}
browserScreen {
}.openThreeDotMenu {
}.refreshPage {
verifyNetworkCacheIsEmpty("memory")
verifyNetworkCacheIsEmpty("disk")
}
setNetworkEnabled(enabled = true)
}
@SmokeTest
@Test
fun saveLoginsInPWATest() {
val pwaPage = "https://mozilla-mobile.github.io/testapp/loginForm"
val shortcutTitle = "TEST_APP"
navigationToolbar {
}.enterURLAndEnterToBrowser(pwaPage.toUri()) {
verifyNotificationDotOnMainMenu()
}.openThreeDotMenu {
}.clickInstall {
clickAddAutomaticallyButton()
}.openHomeScreenShortcut(shortcutTitle) {
mDevice.waitForIdle()
fillAndSubmitLoginCredentials("mozilla", "firefox")
verifySaveLoginPromptIsDisplayed()
saveLoginFromPrompt("Save")
openAppFromExternalLink(pwaPage)
browserScreen {
}.openThreeDotMenu {
}.openSettings {
}.openLoginsAndPasswordSubMenu {
}.openSavedLogins {
verifySecurityPromptForLogins()
tapSetupLater()
verifySavedLoginsSectionUsername("mozilla")
}
addToHomeScreen {
}.searchAndOpenHomeScreenShortcut(shortcutTitle) {
verifyPrefilledPWALoginCredentials("mozilla", shortcutTitle)
}
}
}
@SmokeTest
@Test
fun verifyCookieBannerReductionTest() {
val webSite = "voetbal24.be"
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(webSite.toUri()) {
waitForPageToLoad()
verifyCookieBannerExists(exists = true)
}.openThreeDotMenu {
}.openSettings {
}.openCookieBannerReductionSubMenu {
verifyCookieBannerView(isCookieBannerReductionChecked = false)
clickCookieBannerReductionToggle()
verifyCheckedCookieBannerReductionToggle(isCookieBannerReductionChecked = true)
}
exitMenu()
browserScreen {
verifyCookieBannerExists(exists = false)
}
restartApp(activityTestRule)
browserScreen {
verifyCookieBannerExists(exists = false)
}.openThreeDotMenu {
}.openSettings {
}.openCookieBannerReductionSubMenu {
clickCookieBannerReductionToggle()
verifyCheckedCookieBannerReductionToggle(false)
}
exitMenu()
browserScreen {
}.openThreeDotMenu {
}.refreshPage {
verifyCookieBannerExists(exists = false)
}
}
@SmokeTest
@Test
fun verifyCookieBannerReductionInPrivateBrowsingTest() {
val webSite = "voetbal24.be"
homeScreen {
}.togglePrivateBrowsingMode()
navigationToolbar {
}.enterURLAndEnterToBrowser(webSite.toUri()) {
waitForPageToLoad()
verifyCookieBannerExists(exists = true)
}.openThreeDotMenu {
}.openSettings {
}.openCookieBannerReductionSubMenu {
verifyCookieBannerView(isCookieBannerReductionChecked = false)
clickCookieBannerReductionToggle()
verifyCheckedCookieBannerReductionToggle(isCookieBannerReductionChecked = true)
exitMenu()
}
browserScreen {
verifyCookieBannerExists(exists = false)
}
restartApp(activityTestRule)
homeScreen {
}.openTabDrawer {
}.openTab("Voetbal24") {
verifyCookieBannerExists(exists = false)
}.openThreeDotMenu {
}.openSettings {
}.openCookieBannerReductionSubMenu {
clickCookieBannerReductionToggle()
verifyCheckedCookieBannerReductionToggle(false)
exitMenu()
}
browserScreen {
}.openThreeDotMenu {
}.refreshPage {
verifyCookieBannerExists(exists = false)
}
}
}
private fun setOpenLinksInPrivateOn() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openPrivateBrowsingSubMenu {
verifyOpenLinksInPrivateTabEnabled()
clickOpenLinksInPrivateTabSwitch()
}.goBack {
}.goBack {
verifyHomeComponent()
}
}
private fun setOpenLinksInPrivateOff() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openPrivateBrowsingSubMenu {
clickOpenLinksInPrivateTabSwitch()
verifyOpenLinksInPrivateTabOff()
}.goBack {
}.goBack {
verifyHomeComponent()
}
}

@ -0,0 +1,190 @@
/* 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 okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.ui.robots.addToHomeScreen
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
class SettingsPrivateBrowsingTest {
private lateinit var mockWebServer: MockWebServer
private val pageShortcutName = TestHelper.generateRandomString(5)
@get:Rule
val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
@Before
fun setUp() {
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
@Test
fun verifyPrivateBrowsingMenuItemsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openPrivateBrowsingSubMenu {
verifyAddPrivateBrowsingShortcutButton()
verifyOpenLinksInPrivateTab()
verifyOpenLinksInPrivateTabOff()
}.goBack {
verifySettingsView()
}
}
@Test
fun openExternalLinksInPrivateTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
setOpenLinksInPrivateOn()
TestHelper.openAppFromExternalLink(firstWebPage.url.toString())
browserScreen {
verifyUrl(firstWebPage.url.toString())
}.openTabDrawer {
verifyPrivateModeSelected()
}.closeTabDrawer {
}.goToHomescreen { }
setOpenLinksInPrivateOff()
// We need to open a different link, otherwise it will open the same session
TestHelper.openAppFromExternalLink(secondWebPage.url.toString())
browserScreen {
verifyUrl(secondWebPage.url.toString())
}.openTabDrawer {
verifyNormalModeSelected()
}
}
@Test
fun launchPageShortcutInPrivateModeTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
setOpenLinksInPrivateOn()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
}.openAddToHomeScreen {
addShortcutName(pageShortcutName)
clickAddShortcutButton()
clickAddAutomaticallyButton()
verifyShortcutAdded(pageShortcutName)
}
mDevice.waitForIdle()
// We need to close the existing tab here, to open a different session
TestHelper.restartApp(activityTestRule)
browserScreen {
}.openTabDrawer {
closeTab()
}
addToHomeScreen {
}.searchAndOpenHomeScreenShortcut(pageShortcutName) {
}.openTabDrawer {
verifyPrivateModeSelected()
}
}
@Test
fun launchLinksInPrivateToggleOffStateDoesntChangeTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
setOpenLinksInPrivateOn()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
expandMenu()
}.openAddToHomeScreen {
addShortcutName(pageShortcutName)
clickAddShortcutButton()
clickAddAutomaticallyButton()
}.openHomeScreenShortcut(pageShortcutName) {
}.goToHomescreen { }
setOpenLinksInPrivateOff()
TestHelper.restartApp(activityTestRule)
mDevice.waitForIdle()
addToHomeScreen {
}.searchAndOpenHomeScreenShortcut(pageShortcutName) {
}.openTabDrawer {
verifyNormalModeSelected()
}.closeTabDrawer {
}.openThreeDotMenu {
}.openSettings {
}.openPrivateBrowsingSubMenu {
verifyOpenLinksInPrivateTabOff()
}
}
@Test
fun addPrivateBrowsingShortcut() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openPrivateBrowsingSubMenu {
cancelPrivateShortcutAddition()
addPrivateShortcutToHomescreen()
verifyPrivateBrowsingShortcutIcon()
}.openPrivateBrowsingShortcut {
verifySearchView()
}.openBrowser {
}.openTabDrawer {
verifyPrivateModeSelected()
}
}
}
private fun setOpenLinksInPrivateOn() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openPrivateBrowsingSubMenu {
verifyOpenLinksInPrivateTabEnabled()
clickOpenLinksInPrivateTabSwitch()
}.goBack {
}.goBack {
verifyHomeComponent()
}
}
private fun setOpenLinksInPrivateOff() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openPrivateBrowsingSubMenu {
clickOpenLinksInPrivateTabSwitch()
verifyOpenLinksInPrivateTabOff()
}.goBack {
}.goBack {
verifyHomeComponent()
}
}

@ -0,0 +1,52 @@
package org.mozilla.fenix.ui
import androidx.test.filters.SdkSuppress
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.ui.robots.homeScreen
class SettingsSitePermissionsTest {
@get:Rule
val activityTestRule = HomeActivityTestRule.withDefaultSettingsOverrides()
// Verifies that you can go to System settings and change app's permissions from inside the app
@SmokeTest
@Test
@SdkSuppress(minSdkVersion = 29)
fun redirectToAppPermissionsSystemSettingsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuSitePermissions {
}.openCamera {
verifyBlockedByAndroid()
}.goBack {
}.openLocation {
verifyBlockedByAndroid()
}.goBack {
}.openMicrophone {
verifyBlockedByAndroid()
clickGoToSettingsButton()
openAppSystemPermissionsSettings()
switchAppPermissionSystemSetting("Camera", "Allow")
goBackToSystemAppPermissionSettings()
verifySystemGrantedPermission("Camera")
switchAppPermissionSystemSetting("Location", "Allow")
goBackToSystemAppPermissionSettings()
verifySystemGrantedPermission("Location")
switchAppPermissionSystemSetting("Microphone", "Allow")
goBackToSystemAppPermissionSettings()
verifySystemGrantedPermission("Microphone")
goBackToPermissionsSettingsSubMenu()
verifyUnblockedByAndroid()
}.goBack {
}.openLocation {
verifyUnblockedByAndroid()
}.goBack {
}.openCamera {
verifyUnblockedByAndroid()
}
}
}

@ -14,6 +14,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.ui.robots.homeScreen
/**
* Tests for verifying the main three dot menu options
@ -27,7 +28,7 @@ class SettingsTest {
private lateinit var mockWebServer: MockWebServer
@get:Rule
val activityTestRule = HomeActivityTestRule()
val activityTestRule = HomeActivityTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
@Before
fun setUp() {
@ -43,6 +44,111 @@ class SettingsTest {
mockWebServer.shutdown()
}
// Walks through settings privacy menu and sub-menus to ensure all items are present
@Test
fun settingsPrivacyItemsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
// PRIVACY
verifyPrivacyHeading()
// PRIVATE BROWSING
verifyPrivateBrowsingButton()
}.openPrivateBrowsingSubMenu {
verifyNavigationToolBarHeader()
}.goBack {
// HTTPS-Only Mode
verifyHTTPSOnlyModeButton()
verifyHTTPSOnlyModeState("Off")
// ENHANCED TRACKING PROTECTION
verifyEnhancedTrackingProtectionButton()
verifyEnhancedTrackingProtectionState("Standard")
}.openEnhancedTrackingProtectionSubMenu {
verifyNavigationToolBarHeader()
verifyEnhancedTrackingProtectionProtectionSubMenuItems()
// ENHANCED TRACKING PROTECTION EXCEPTION
}.openExceptions {
verifyNavigationToolBarHeader()
verifyEnhancedTrackingProtectionProtectionExceptionsSubMenuItems()
}.goBack {
}.goBack {
// SITE PERMISSIONS
verifySitePermissionsButton()
}.openSettingsSubMenuSitePermissions {
verifyNavigationToolBarHeader()
verifySitePermissionsSubMenuItems()
// SITE PERMISSIONS AUTOPLAY
}.openAutoPlay {
verifyNavigationToolBarHeader("Autoplay")
verifySitePermissionsAutoPlaySubMenuItems()
}.goBack {
// SITE PERMISSIONS CAMERA
}.openCamera {
verifyNavigationToolBarHeader("Camera")
verifySitePermissionsCommonSubMenuItems()
verifyToggleNameToON("3. Toggle Camera to ON")
}.goBack {
// SITE PERMISSIONS LOCATION
}.openLocation {
verifyNavigationToolBarHeader("Location")
verifySitePermissionsCommonSubMenuItems()
verifyToggleNameToON("3. Toggle Location to ON")
}.goBack {
// SITE PERMISSIONS MICROPHONE
}.openMicrophone {
verifyNavigationToolBarHeader("Microphone")
verifySitePermissionsCommonSubMenuItems()
verifyToggleNameToON("3. Toggle Microphone to ON")
}.goBack {
// SITE PERMISSIONS NOTIFICATION
}.openNotification {
verifyNavigationToolBarHeader("Notification")
verifySitePermissionsNotificationSubMenuItems()
}.goBack {
// SITE PERMISSIONS PERSISTENT STORAGE
}.openPersistentStorage {
verifyNavigationToolBarHeader("Persistent Storage")
verifySitePermissionsPersistentStorageSubMenuItems()
}.goBack {
// SITE PERMISSIONS EXCEPTIONS
}.openExceptions {
verifyNavigationToolBarHeader()
verifySitePermissionsExceptionSubMenuItems()
}.goBack {
}.goBack {
// DELETE BROWSING DATA
verifyDeleteBrowsingDataButton()
}.openSettingsSubMenuDeleteBrowsingData {
verifyNavigationToolBarHeader()
verifyDeleteBrowsingDataSubMenuItems()
}.goBack {
// DELETE BROWSING DATA ON QUIT
verifyDeleteBrowsingDataOnQuitButton()
verifyDeleteBrowsingDataOnQuitState("Off")
}.openSettingsSubMenuDeleteBrowsingDataOnQuit {
verifyNavigationToolBarHeader()
verifyDeleteBrowsingDataOnQuitSubMenuItems()
}.goBack {
// NOTIFICATIONS
verifyNotificationsButton()
}.openSettingsSubMenuNotifications {
verifySystemNotificationsView()
}.goBack {
// DATA COLLECTION
verifyDataCollectionButton()
}.openSettingsSubMenuDataCollection {
verifyNavigationToolBarHeader()
verifyDataCollectionSubMenuItems()
}.goBack {
}.goBack {
verifyHomeComponent()
}
}
// Walks through settings menu and sub-menus to ensure all items are present
@Ignore("This is a stub test, ignore for now")
@Test

@ -29,10 +29,8 @@ import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
import org.mozilla.fenix.helpers.RetryTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.TestHelper.assertNativeAppOpens
import org.mozilla.fenix.helpers.TestHelper.createCustomTabIntent
import org.mozilla.fenix.helpers.TestHelper.generateRandomString
import org.mozilla.fenix.helpers.TestHelper.registerAndCleanupIdlingResources
import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource
import org.mozilla.fenix.ui.robots.browserScreen
@ -220,35 +218,6 @@ class SmokeTest {
}
}
// Verifies the Add to home screen option in a tab's 3 dot menu
@Test
fun mainMenuAddToHomeScreenTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val shortcutTitle = generateRandomString(5)
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
}.openThreeDotMenu {
expandMenu()
}.openAddToHomeScreen {
clickCancelShortcutButton()
}
browserScreen {
}.openThreeDotMenu {
expandMenu()
}.openAddToHomeScreen {
verifyShortcutTextFieldTitle("Test_Page_1")
addShortcutName(shortcutTitle)
clickAddShortcutButton()
clickAddAutomaticallyButton()
}.openHomeScreenShortcut(shortcutTitle) {
verifyUrl(website.url.toString())
verifyTabCounter("1")
}
}
// Verifies the Add to collection option in a tab's 3 dot menu
@Test
fun openMainMenuAddToCollectionTest() {
@ -538,25 +507,6 @@ class SmokeTest {
}
}
@Test
fun addPrivateBrowsingShortcutTest() {
homeScreen {
}.dismissOnboarding()
homeScreen {
}.triggerPrivateBrowsingShortcutPrompt {
verifyNoThanksPrivateBrowsingShortcutButton()
verifyAddPrivateBrowsingShortcutButton()
clickAddPrivateBrowsingShortcutButton()
clickAddAutomaticallyButton()
}.openHomeScreenShortcut("Private $appName") {}
searchScreen {
verifySearchView()
}.dismissSearchBar {
verifyPrivateSessionMessage()
}
}
@Test
fun mainMenuInstallPWATest() {
val pwaPage = "https://mozilla-mobile.github.io/testapp/"

@ -352,7 +352,7 @@ class TabbedBrowsingTest {
verifyFocusedNavigationToolbar()
// dismiss search dialog
homeScreen { }.pressBack()
verifyPrivateSessionMessage()
verifyCommonMythsLink()
verifyNavigationToolbar()
}
navigationToolbar {

@ -45,6 +45,7 @@ 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.MatcherHelper.assertItemWithResIdAndTextExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdExists
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
@ -413,6 +414,7 @@ class BrowserRobot {
mDevice.findObject(
UiSelector().resourceId("$packageName:id/feature_prompt_login_fragment"),
),
waitingTime,
)
mDevice.findObject(text(optionToSaveLogin)).click()
}
@ -493,7 +495,7 @@ class BrowserRobot {
setPageObjectText(webPageItemWithResourceId("password"), password)
clickPageObject(webPageItemWithResourceId("submit"))
mDevice.waitForObjects(mDevice.findObject(UiSelector().resourceId("$packageName:id/save_confirm")))
mDevice.waitForObjects(mDevice.findObject(UiSelector().resourceId("$packageName:id/save_confirm")), waitingTime)
}
fun clearUserNameLoginCredential() {
@ -543,13 +545,47 @@ class BrowserRobot {
fun verifySelectAddressButtonExists(exists: Boolean) = assertItemWithResIdExists(selectAddressButton, exists = exists)
fun clickCardNumberTextBox() = clickPageObject(webPageItemWithResourceId("cardNumber"))
fun clickCreditCardNumberTextBox() = clickPageObject(webPageItemWithResourceId("cardNumber"))
fun changeCreditCardExpiryDate(expiryDate: String) =
webPageItemWithResourceId("expiryMonthAndYear").setText(expiryDate)
fun clickCreditCardFormSubmitButton() =
webPageItemWithResourceId("submit").clickAndWaitForNewWindow(waitingTime)
fun fillAndSaveCreditCard(cardNumber: String, cardName: String, expiryMonthAndYear: String) {
webPageItemWithResourceId("cardNumber").setText(cardNumber)
webPageItemWithResourceId("nameOnCard").setText(cardName)
webPageItemWithResourceId("expiryMonthAndYear").setText(expiryMonthAndYear)
webPageItemWithResourceId("submit").clickAndWaitForNewWindow(waitingTime)
}
fun clickCancelCreditCardPromptButton() =
itemWithResId("$packageName:id/save_cancel").also {
it.waitForExists(waitingTime)
it.click()
}
fun clickUpdateOrSaveCreditCardPromptButton() =
itemWithResId("$packageName:id/save_confirm").also {
it.waitForExists(waitingTime)
it.click()
}
fun verifyUpdateOrSaveCreditCardPromptExists(exists: Boolean) =
assertItemWithResIdAndTextExists(
itemWithResId("$packageName:id/save_credit_card_header"),
exists = exists,
)
fun clickSelectCreditCardButton() {
selectCreditCardButton.waitForExists(waitingTime)
selectCreditCardButton.clickAndWaitForNewWindow(waitingTime)
}
fun verifySelectCreditCardPromptExists(exists: Boolean) =
assertItemWithResIdExists(selectCreditCardButton, exists = exists)
fun clickLoginSuggestion(userName: String) {
val loginSuggestion =
mDevice.findObject(
@ -571,6 +607,12 @@ class BrowserRobot {
creditCardSuggestion(creditCardNumber).click()
}
fun verifyCreditCardSuggestion(vararg creditCardNumbers: String) {
for (creditCardNumber in creditCardNumbers) {
assertTrue(creditCardSuggestion(creditCardNumber).waitForExists(waitingTime))
}
}
fun verifySuggestedUserName(userName: String) {
mDevice.findObject(
UiSelector()
@ -966,6 +1008,12 @@ class BrowserRobot {
it.click()
}
fun verifyFindInPageBar(exists: Boolean) =
assertItemWithResIdExists(
itemWithResId("$packageName:id/findInPageView"),
exists = exists,
)
class Transition {
private fun threeDotButton() = onView(
allOf(
@ -984,9 +1032,10 @@ class BrowserRobot {
}
fun openNavigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition {
mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
.waitForExists(waitingTime)
navURLBar().waitForExists(waitingTime)
navURLBar().click()
mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_url_view"))
.waitForExists(waitingTime)
NavigationToolbarRobot().interact()
return NavigationToolbarRobot.Transition()
@ -1167,6 +1216,13 @@ class BrowserRobot {
SettingsSubMenuAutofillRobot().interact()
return SettingsSubMenuAutofillRobot.Transition()
}
fun clickManageCreditCardsButton(interact: SettingsSubMenuAutofillRobot.() -> Unit): SettingsSubMenuAutofillRobot.Transition {
itemWithResId("$packageName:id/manage_credit_cards").clickAndWaitForNewWindow(waitingTime)
SettingsSubMenuAutofillRobot().interact()
return SettingsSubMenuAutofillRobot.Transition()
}
}
}
@ -1298,7 +1354,8 @@ private fun setPageObjectText(webPageItem: UiObject, text: String) {
try {
webPageItem.also {
it.waitForExists(waitingTime)
it.setText(text)
it.clearTextField()
it.text = text
}
break

@ -14,7 +14,6 @@ import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeLeft
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
@ -25,8 +24,14 @@ 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.assertItemContainingTextExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithDescriptionExists
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
import org.mozilla.fenix.helpers.TestHelper.getStringResource
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
@ -197,13 +202,9 @@ class CollectionRobot {
title: String,
interact: HomeScreenRobot.() -> Unit,
): HomeScreenRobot.Transition {
try {
collectionTitle(title).waitForExists(waitingTime)
collectionTitle(title).click()
} catch (e: NoMatchingViewException) {
scrollToElementByText(title)
collectionTitle(title).click()
}
assertItemContainingTextExists(itemContainingText(title))
itemContainingText(title).clickAndWaitForNewWindow(waitingTimeShort)
assertItemWithDescriptionExists(itemWithDescription(getStringResource(R.string.remove_tab_from_collection)), exists = false)
HomeScreenRobot().interact()
return HomeScreenRobot.Transition()

@ -143,6 +143,13 @@ class CustomTabRobot {
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun goBackToOnboardingScreen(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
mDevice.pressBack()
HomeScreenRobot().interact()
return HomeScreenRobot.Transition()
}
}
}

@ -58,12 +58,23 @@ class FindInPageRobot {
}
class Transition {
fun closeFindInPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
fun closeFindInPageWithCloseButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitForIdle()
findInPageCloseButton().click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun closeFindInPageWithBackButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitForIdle()
// Will need to press back 2x, the first will only dismiss the keyboard
mDevice.pressBack()
mDevice.pressBack()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
}
}

@ -37,6 +37,7 @@ 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
@ -55,11 +56,13 @@ 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.assertItemWithDescriptionExists
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.itemWithDescription
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndDescription
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
@ -74,6 +77,7 @@ import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.helpers.withBitmapDrawable
import org.mozilla.fenix.utils.Settings
/**
* Implementation of Robot Pattern for the home screen menu.
@ -89,6 +93,13 @@ class HomeScreenRobot {
fun verifyFocusedNavigationToolbar() = assertFocusedNavigationToolbar()
fun verifyHomeScreen() = assertItemWithResIdExists(homeScreen)
fun verifyPrivateBrowsingHomeScreen() {
verifyHomeScreenAppBarItems()
assertItemContainingTextExists(itemContainingText(privateSessionMessage))
verifyCommonMythsLink()
verifyNavigationToolbarItems()
}
fun verifyHomeScreenAppBarItems() =
assertItemWithResIdExists(homeScreen, privateBrowsingButton, homepageWordmark)
@ -151,9 +162,9 @@ class HomeScreenRobot {
assertItemWithResIdExists(signInButton)
}
fun verifyPrivacyProtectionCard(isStandardChecked: Boolean, isStrictChecked: Boolean) {
fun verifyPrivacyProtectionCard(settings: Settings, isStandardChecked: Boolean, isStrictChecked: Boolean) {
scrollToElementByText(getStringResource(R.string.onboarding_privacy_notice_header_1))
assertItemContainingTextExists(privacyProtectionHeader, privacyProtectionDescription)
assertItemContainingTextExists(privacyProtectionHeader, privacyProtectionDescription(settings))
assertCheckedItemWithResIdExists(
standardTrackingProtectionToggle(isStandardChecked),
strictTrackingProtectionToggle(isStrictChecked),
@ -178,7 +189,7 @@ class HomeScreenRobot {
assertItemContainingTextExists(conclusionHeader)
}
fun verifyNavigationToolbarItems(numberOfOpenTabs: String) {
fun verifyNavigationToolbarItems(numberOfOpenTabs: String = "0") {
assertItemWithResIdExists(navigationToolbar, menuButton)
assertItemWithResIdAndTextExists(tabCounter(numberOfOpenTabs))
}
@ -262,7 +273,8 @@ class HomeScreenRobot {
.onNodeWithText(getStringResource(R.string.onboarding_home_skip_button))
.performClick()
fun verifyPrivateSessionMessage() = assertPrivateSessionMessage()
fun verifyCommonMythsLink() =
assertItemContainingTextExists(itemContainingText(getStringResource(R.string.private_browsing_common_myths)))
fun verifyExistingTopSitesList() = assertExistingTopSitesList()
fun verifyNotExistingTopSitesList(title: String) = assertNotExistingTopSitesList(title)
@ -291,8 +303,10 @@ class HomeScreenRobot {
assertTrue(jumpBackInSection().waitForExists(waitingTime))
}
fun verifyJumpBackInSectionIsNotDisplayed() = assertJumpBackInSectionIsNotDisplayed()
fun verifyJumpBackInItemTitle(itemTitle: String) = assertJumpBackInItemTitle(itemTitle)
fun verifyJumpBackInItemWithUrl(itemUrl: String) = assertJumpBackInItemWithUrl(itemUrl)
fun verifyJumpBackInItemTitle(testRule: ComposeTestRule, itemTitle: String) =
assertJumpBackInItemTitle(testRule, itemTitle)
fun verifyJumpBackInItemWithUrl(testRule: ComposeTestRule, itemUrl: String) =
assertJumpBackInItemWithUrl(testRule, itemUrl)
fun verifyJumpBackInShowAllButton() = assertJumpBackInShowAllButton()
fun verifyRecentlyVisitedSectionIsDisplayed() = assertRecentlyVisitedSectionIsDisplayed()
fun verifyRecentlyVisitedSectionIsNotDisplayed() = assertRecentlyVisitedSectionIsNotDisplayed()
@ -322,10 +336,8 @@ class HomeScreenRobot {
// Collections elements
fun verifyCollectionIsDisplayed(title: String, collectionExists: Boolean = true) {
if (collectionExists) {
scrollToElementByText(title)
assertTrue(mDevice.findObject(UiSelector().text(title)).waitForExists(waitingTime))
} else {
scrollToElementByText("Collections")
assertTrue(mDevice.findObject(UiSelector().text(title)).waitUntilGone(waitingTime))
}
}
@ -721,13 +733,10 @@ class HomeScreenRobot {
return TabDrawerRobot.Transition()
}
fun expandCollection(title: String, rule: ComposeTestRule, interact: CollectionRobot.() -> Unit): CollectionRobot.Transition {
homeScreenList().waitForExists(waitingTime)
homeScreenList().scrollToEnd(LISTS_MAXSWIPES)
collectionTitle(title, rule)
.assertIsDisplayed()
.performClick()
fun expandCollection(title: String, interact: CollectionRobot.() -> Unit): CollectionRobot.Transition {
assertItemContainingTextExists(itemContainingText(title))
itemContainingText(title).clickAndWaitForNewWindow(waitingTimeShort)
assertItemWithDescriptionExists(itemWithDescription(getStringResource(R.string.remove_tab_from_collection)))
CollectionRobot().interact()
return CollectionRobot.Transition()
@ -820,11 +829,11 @@ class HomeScreenRobot {
return SyncSignInRobot.Transition()
}
fun clickPrivacyNoticeButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
fun clickPrivacyNoticeButton(interact: CustomTabRobot.() -> Unit): CustomTabRobot.Transition {
privacyNoticeButton.clickAndWaitForNewWindow(waitingTimeShort)
BrowserRobot().interact()
return BrowserRobot.Transition()
CustomTabRobot().interact()
return CustomTabRobot.Transition()
}
}
}
@ -891,16 +900,6 @@ private fun verifySearchEngineIcon(searchEngineName: String) {
verifySearchEngineIcon(defaultSearchEngine.icon, defaultSearchEngine.name)
}
private fun assertPrivateSessionMessage() =
assertTrue(
mDevice.findObject(
UiSelector()
.textContains(
getStringResource(R.string.private_browsing_common_myths),
),
).waitForExists(waitingTime),
)
private fun collectionTitle(title: String, rule: ComposeTestRule) =
rule.onNode(hasText(title))
@ -991,25 +990,11 @@ private fun assertTopSiteContextMenuItems() {
private fun assertJumpBackInSectionIsNotDisplayed() = assertFalse(jumpBackInSection().waitForExists(waitingTimeShort))
private fun assertJumpBackInItemTitle(itemTitle: String) =
assertTrue(
mDevice
.findObject(
UiSelector()
.resourceId("recent.tab.title")
.textContains(itemTitle),
).waitForExists(waitingTime),
)
private fun assertJumpBackInItemTitle(testRule: ComposeTestRule, itemTitle: String) =
testRule.onNodeWithTag("recent.tab.title", useUnmergedTree = true).assert(hasText(itemTitle))
private fun assertJumpBackInItemWithUrl(itemUrl: String) =
assertTrue(
mDevice
.findObject(
UiSelector()
.resourceId("recent.tab.url")
.textContains(itemUrl),
).waitForExists(waitingTime),
)
private fun assertJumpBackInItemWithUrl(testRule: ComposeTestRule, itemUrl: String) =
testRule.onNodeWithTag("recent.tab.url", useUnmergedTree = true).assert(hasText(itemUrl))
private fun assertJumpBackInShowAllButton() =
assertTrue(
@ -1060,6 +1045,16 @@ private fun sponsoredShortcut(sponsoredShortcutTitle: String) =
private fun storyByTopicItem(composeTestRule: ComposeTestRule, position: Int) =
composeTestRule.onNodeWithTag("pocket.categories").onChildAt(position - 1)
private fun privacyProtectionDescription(settings: Settings): UiObject {
val isTCPPublic = settings.enabledTotalCookieProtectionCFR
val descriptionText = when (isTCPPublic) {
true -> R.string.onboarding_tracking_protection_description
false -> R.string.onboarding_tracking_protection_description_old
}
return itemContainingText(getStringResource(descriptionText))
}
private val homeScreen =
itemWithResId("$packageName:id/homeLayout")
private val privateBrowsingButton =
@ -1112,8 +1107,6 @@ private val signInButton =
itemWithResId("$packageName:id/fxa_sign_in_button")
private val privacyProtectionHeader =
itemContainingText(getStringResource(R.string.onboarding_tracking_protection_header))
private val privacyProtectionDescription =
itemContainingText(getStringResource(R.string.onboarding_tracking_protection_description))
private fun standardTrackingProtectionToggle(isChecked: Boolean) =
checkedItemWithResId("$packageName:id/tracking_protection_standard_option", isChecked)
private fun strictTrackingProtectionToggle(isChecked: Boolean) =

@ -44,6 +44,8 @@ import org.mozilla.fenix.helpers.ext.waitNotNull
* Implementation of Robot Pattern for the URL toolbar.
*/
class NavigationToolbarRobot {
fun verifyUrl(url: String) =
onView(withId(R.id.mozac_browser_toolbar_url_view)).check(matches(withText(url)))
fun verifyNoHistoryBookmarks() = assertNoHistoryBookmarks()

@ -22,11 +22,8 @@ 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.hasSibling
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.isNotChecked
import androidx.test.espresso.matcher.ViewMatchers.withClassName
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
@ -39,7 +36,6 @@ import androidx.test.uiautomator.Until
import junit.framework.AssertionFailedError
import org.hamcrest.CoreMatchers
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.endsWith
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.Constants.LISTS_MAXSWIPES
@ -49,12 +45,12 @@ import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.TestHelper.getStringResource
import org.mozilla.fenix.helpers.TestHelper.hasCousin
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.scrollToElementByText
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.ui.robots.SettingsRobot.Companion.DEFAULT_APPS_SETTINGS_ACTION
@ -97,8 +93,7 @@ class SettingsRobot {
fun verifyNotificationsButton() = assertNotificationsButton()
fun verifyDataCollectionButton() = assertDataCollectionButton()
fun verifyOpenLinksInAppsButton() = assertOpenLinksInAppsButton()
fun verifyOpenLinksInAppsSwitchState(enabled: Boolean) = assertOpenLinksInAppsSwitchState(enabled)
fun clickOpenLinksInAppsSwitch() = openLinksInAppsButton().click()
fun verifyOpenLinksInAppsState(state: String) = assertOpenLinksInAppsSwitchState(state)
fun verifySettingsView() = assertSettingsView()
fun verifySettingsToolbar() = assertSettingsToolbar()
@ -173,8 +168,11 @@ class SettingsRobot {
}
fun openAutofillSubMenu(interact: SettingsSubMenuAutofillRobot.() -> Unit): SettingsSubMenuAutofillRobot.Transition {
mDevice.findObject(UiSelector().textContains(getStringResource(R.string.preferences_autofill))).waitForExists(waitingTime)
onView(withText(R.string.preferences_autofill)).click()
mDevice.findObject(UiSelector().textContains(getStringResource(R.string.preferences_autofill)))
.also {
it.waitForExists(waitingTime)
it.click()
}
SettingsSubMenuAutofillRobot().interact()
return SettingsSubMenuAutofillRobot.Transition()
@ -314,6 +312,25 @@ class SettingsRobot {
SettingsSubMenuAddonsManagerRobot().interact()
return SettingsSubMenuAddonsManagerRobot.Transition()
}
fun openOpenLinksInAppsMenu(interact: SettingsSubMenuOpenLinksInAppsRobot.() -> Unit): SettingsSubMenuOpenLinksInAppsRobot.Transition {
openLinksInAppsButton().click()
SettingsSubMenuOpenLinksInAppsRobot().interact()
return SettingsSubMenuOpenLinksInAppsRobot.Transition()
}
fun openHttpsOnlyModeMenu(interact: SettingsSubMenuHttpsOnlyModeRobot.() -> Unit): SettingsSubMenuHttpsOnlyModeRobot.Transition {
scrollToElementByText("HTTPS-Only Mode")
onView(withText(getStringResource(R.string.preferences_https_only_title))).click()
mDevice.waitNotNull(
Until.findObjects(By.res("$packageName:id/https_only_switch")),
waitingTime,
)
SettingsSubMenuHttpsOnlyModeRobot().interact()
return SettingsSubMenuHttpsOnlyModeRobot.Transition()
}
}
companion object {
@ -501,33 +518,13 @@ private fun assertOpenLinksInAppsButton() {
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
fun assertOpenLinksInAppsSwitchState(enabled: Boolean) {
scrollToElementByText("Open links in apps")
if (enabled) {
openLinksInAppsButton()
.check(
matches(
hasCousin(
allOf(
withClassName(endsWith("Switch")),
isChecked(),
),
),
),
)
} else {
openLinksInAppsButton()
.check(
matches(
hasCousin(
allOf(
withClassName(endsWith("Switch")),
isNotChecked(),
),
),
),
)
}
fun assertOpenLinksInAppsSwitchState(state: String) {
onView(
allOf(
withText(R.string.preferences_open_links_in_apps),
hasSibling(withText(state)),
),
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
// DEVELOPER TOOLS SECTION

@ -15,6 +15,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.uiautomator.UiSelector
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.endsWith
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists
@ -53,6 +54,25 @@ class SettingsSubMenuAutofillRobot {
verifyAddressesAutofillToggle(isAddressAutofillEnabled)
}
fun verifyCreditCardsAutofillSection(isAddressAutofillEnabled: Boolean, userHasSavedCreditCard: Boolean) {
assertItemContainingTextExists(
autofillToolbarTitle,
creditCardsSectionTitle,
saveAndAutofillCreditCardsOption,
saveAndAutofillCreditCardsSummary,
syncCreditCardsAcrossDevicesButton,
)
if (userHasSavedCreditCard) {
assertItemContainingTextExists(manageSavedCreditCardsButton)
} else {
assertItemContainingTextExists(addCreditCardButton)
}
verifySaveAndAutofillCreditCardsToggle(isAddressAutofillEnabled)
}
fun verifyManageAddressesSection(vararg savedAddressDetails: String) {
assertItemWithDescriptionExists(navigateBackButton)
assertItemContainingTextExists(
@ -68,6 +88,16 @@ class SettingsSubMenuAutofillRobot {
}
}
fun verifySavedCreditCardsSection(creditCardLastDigits: String, creditCardExpiryDate: String) {
assertItemWithDescriptionExists(navigateBackButton)
assertItemContainingTextExists(
savedCreditCardsToolbarTitle,
addCreditCardButton,
itemContainingText(creditCardLastDigits),
itemContainingText(creditCardExpiryDate),
)
}
fun verifyAddressesAutofillToggle(enabled: Boolean) =
onView(withText(R.string.preferences_addresses_save_and_autofill_addresses))
.check(
@ -85,6 +115,23 @@ class SettingsSubMenuAutofillRobot {
),
)
fun verifySaveAndAutofillCreditCardsToggle(enabled: Boolean) =
onView(withText(R.string.preferences_credit_cards_save_and_autofill_cards))
.check(
matches(
hasCousin(
allOf(
withClassName(endsWith("Switch")),
if (enabled) {
isChecked()
} else {
isNotChecked()
},
),
),
),
)
fun verifyAddAddressView() {
assertItemContainingTextExists(addAddressToolbarTitle)
assertItemWithDescriptionExists(navigateBackButton)
@ -227,16 +274,23 @@ class SettingsSubMenuAutofillRobot {
}
fun clickAddCreditCardButton() = addCreditCardButton.click()
fun clickManageSavedCardsButton() = manageSavedCardsButton.click()
fun clickManageSavedCreditCardsButton() = manageSavedCreditCardsButton.click()
fun clickSecuredCreditCardsLaterButton() = securedCreditCardsLaterButton.click()
fun clickSavedCreditCard() = savedCreditCardNumber.clickAndWaitForNewWindow(waitingTime)
fun clickDeleteCreditCardButton() {
deleteCreditCardButton.waitForExists(waitingTime)
deleteCreditCardButton.click()
fun clickDeleteCreditCardToolbarButton() {
deleteCreditCardToolbarButton.waitForExists(waitingTime)
deleteCreditCardToolbarButton.click()
}
fun clickDeleteCreditCardMenuButton() {
deleteCreditCardMenuButton.waitForExists(waitingTime)
deleteCreditCardMenuButton.click()
}
fun clickSaveAndAutofillCreditCardsOption() = saveAndAutofillCreditCardsOption.click()
fun clickConfirmDeleteCreditCardButton() = confirmDeleteCreditCardButton.click()
fun clickCancelDeleteCreditCardButton() = cancelDeleteCreditCardButton.click()
fun clickExpiryMonthOption(expiryMonth: String) {
expiryMonthOption(expiryMonth).waitForExists(waitingTime)
expiryMonthOption(expiryMonth).click()
@ -250,18 +304,76 @@ class SettingsSubMenuAutofillRobot {
fun verifyAddCreditCardsButton() = assertTrue(addCreditCardButton.waitForExists(waitingTime))
fun fillAndSaveCreditCard(cardNumber: String, cardName: String, expiryMonth: String, expiryYear: String) {
cardNumberTextInput.waitForExists(waitingTime)
cardNumberTextInput.setText(cardNumber)
nameOnCardTextInput.setText(cardName)
creditCardNumberTextInput.waitForExists(waitingTime)
creditCardNumberTextInput.setText(cardNumber)
nameOnCreditCardTextInput.setText(cardName)
expiryMonthDropDown.click()
clickExpiryMonthOption(expiryMonth)
expiryYearDropDown.click()
clickExpiryYearOption(expiryYear)
saveButton.click()
manageSavedCardsButton.waitForExists(waitingTime)
manageSavedCreditCardsButton.waitForExists(waitingTime)
}
fun clearCreditCardNumber() =
creditCardNumberTextInput.also {
it.waitForExists(waitingTime)
it.clearTextField()
}
fun clearNameOnCreditCard() =
nameOnCreditCardTextInput.also {
it.waitForExists(waitingTime)
it.clearTextField()
}
fun clickSaveCreditCardToolbarButton() = saveCreditCardToolbarButton.click()
fun verifyEditCreditCardView(
cardNumber: String,
cardName: String,
expiryMonth: String,
expiryYear: String,
) {
assertItemContainingTextExists(editCreditCardToolbarTitle)
assertItemWithDescriptionExists(navigateBackButton)
assertItemWithResIdExists(
deleteCreditCardToolbarButton,
saveCreditCardToolbarButton,
)
assertEquals(cardNumber, creditCardNumberTextInput.text)
assertEquals(cardName, nameOnCreditCardTextInput.text)
// Can't get the text from the drop-down items, need to verify them individually
assertItemWithResIdExists(
expiryYearDropDown,
expiryMonthDropDown,
)
assertItemContainingTextExists(
itemContainingText(expiryMonth),
itemContainingText(expiryYear),
)
assertItemWithResIdExists(
saveButton,
cancelButton,
)
assertItemContainingTextExists(deleteCreditCardMenuButton)
}
fun verifyEditCreditCardToolbarTitle() = assertItemContainingTextExists(editCreditCardToolbarTitle)
fun verifyCreditCardNumberErrorMessage() =
assertItemContainingTextExists(itemContainingText(getStringResource(R.string.credit_cards_number_validation_error_message)))
fun verifyNameOnCreditCardErrorMessage() =
assertItemContainingTextExists(itemContainingText(getStringResource(R.string.credit_cards_name_on_card_validation_error_message)))
class Transition {
fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
mDevice.pressBack()
@ -277,6 +389,13 @@ class SettingsSubMenuAutofillRobot {
return SettingsSubMenuAutofillRobot.Transition()
}
fun goBackToSavedCreditCards(interact: SettingsSubMenuAutofillRobot.() -> Unit): SettingsSubMenuAutofillRobot.Transition {
navigateBackButton.click()
SettingsSubMenuAutofillRobot().interact()
return SettingsSubMenuAutofillRobot.Transition()
}
fun goBackToBrowser(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.pressBack()
@ -314,15 +433,24 @@ private val toolbarDeleteAddressButton = itemWithResId("$packageName:id/delete_a
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 creditCardsSectionTitle = itemContainingText(getStringResource(R.string.preferences_credit_cards))
private val saveAndAutofillCreditCardsOption = itemContainingText(getStringResource(R.string.preferences_credit_cards_save_and_autofill_cards))
private val saveAndAutofillCreditCardsSummary = itemContainingText(getStringResource(R.string.preferences_credit_cards_save_and_autofill_cards_summary))
private val syncCreditCardsAcrossDevicesButton = itemContainingText(getStringResource(R.string.preferences_credit_cards_sync_cards_across_devices))
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)))
private val cardNumberTextInput = mDevice.findObject(UiSelector().resourceId("$packageName:id/card_number_input"))
private val nameOnCardTextInput = mDevice.findObject(UiSelector().resourceId("$packageName:id/name_on_card_input"))
private val savedCreditCardsToolbarTitle = itemContainingText(getStringResource(R.string.credit_cards_saved_cards))
private val editCreditCardToolbarTitle = itemContainingText(getStringResource(R.string.credit_cards_edit_card))
private val manageSavedCreditCardsButton = mDevice.findObject(UiSelector().textContains(getStringResource(R.string.preferences_credit_cards_manage_saved_cards)))
private val creditCardNumberTextInput = mDevice.findObject(UiSelector().resourceId("$packageName:id/card_number_input"))
private val nameOnCreditCardTextInput = mDevice.findObject(UiSelector().resourceId("$packageName:id/name_on_card_input"))
private val expiryMonthDropDown = mDevice.findObject(UiSelector().resourceId("$packageName:id/expiry_month_drop_down"))
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 deleteCreditCardToolbarButton = mDevice.findObject(UiSelector().resourceId("$packageName:id/delete_credit_card_button"))
private val saveCreditCardToolbarButton = itemWithResId("$packageName:id/save_credit_card_button")
private val deleteCreditCardMenuButton = itemContainingText(getStringResource(R.string.credit_cards_delete_card_button))
private val confirmDeleteCreditCardButton = onView(withId(android.R.id.button1)).inRoot(RootMatchers.isDialog())
private val cancelDeleteCreditCardButton = onView(withId(android.R.id.button2)).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))

@ -0,0 +1,108 @@
/* 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.robots
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.CoreMatchers.allOf
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestHelper.getStringResource
import org.mozilla.fenix.helpers.assertIsChecked
import org.mozilla.fenix.helpers.assertIsEnabled
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.isChecked
class SettingsSubMenuHttpsOnlyModeRobot {
fun verifyHttpsOnlyModeMenuHeader(): ViewInteraction =
onView(
allOf(
withText(getStringResource(R.string.preferences_https_only_title)),
hasSibling(withContentDescription("Navigate up")),
),
).check(matches(isDisplayed()))
fun verifyHttpsOnlyModeSummary() {
onView(withId(R.id.https_only_title))
.check(matches(withText("HTTPS-Only Mode")))
onView(withId(R.id.https_only_summary))
.check(matches(withText("Automatically attempts to connect to sites using HTTPS encryption protocol for increased security. Learn more")))
}
fun verifyHttpsOnlyModeIsEnabled(shouldBeEnabled: Boolean) {
httpsModeOnlySwitch.check(
matches(
if (shouldBeEnabled) {
isChecked(true)
} else {
isChecked(false)
},
),
)
}
fun clickHttpsOnlyModeSwitch() = httpsModeOnlySwitch.click()
fun verifyHttpsOnlyModeOptionsEnabled(shouldBeEnabled: Boolean) {
allTabsOption.assertIsEnabled(shouldBeEnabled)
onlyPrivateTabsOption.assertIsEnabled(shouldBeEnabled)
}
fun verifyHttpsOnlyOptionSelected(allTabsOptionSelected: Boolean, privateTabsOptionSelected: Boolean) {
if (allTabsOptionSelected) {
allTabsOption.assertIsChecked(true)
onlyPrivateTabsOption.assertIsChecked(false)
} else if (privateTabsOptionSelected) {
allTabsOption.assertIsChecked(false)
onlyPrivateTabsOption.assertIsChecked(true)
}
}
fun selectHttpsOnlyModeOption(allTabsOptionSelected: Boolean, privateTabsOptionSelected: Boolean) {
if (allTabsOptionSelected) {
allTabsOption.click()
allTabsOption.assertIsChecked(true)
} else if (privateTabsOptionSelected) {
onlyPrivateTabsOption.click()
onlyPrivateTabsOption.assertIsChecked(true)
}
}
class Transition {
fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
goBackButton.perform(click())
SettingsRobot().interact()
return SettingsRobot.Transition()
}
}
}
private val httpsModeOnlySwitch = onView(withId(R.id.https_only_switch))
private val allTabsOption =
onView(
allOf(
withId(R.id.https_only_all_tabs),
withText("Enable in all tabs"),
),
)
private val onlyPrivateTabsOption =
onView(
allOf(
withId(R.id.https_only_private_tabs),
withText("Enable only in private tabs"),
),
)
private val goBackButton = onView(withContentDescription("Navigate up"))

@ -12,13 +12,14 @@ import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
import androidx.test.espresso.matcher.ViewMatchers.withClassName
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.Until
import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.endsWith
import org.mozilla.fenix.R
@ -35,8 +36,13 @@ import org.mozilla.fenix.helpers.ext.waitNotNull
class SettingsSubMenuLoginsAndPasswordRobot {
fun verifyDefaultView() {
mDevice.waitNotNull(Until.findObjects(By.text("Sync logins across devices")), TestAssetHelper.waitingTime)
assertDefaultView()
mDevice.waitNotNull(Until.findObjects(By.text("Save logins and passwords")), TestAssetHelper.waitingTime)
saveLoginsAndPasswordButton.check(matches(isDisplayed()))
autofillInFirefoxOption.check(matches(isDisplayed()))
autofillInOtherAppsOption.check(matches(isDisplayed()))
syncLoginsButton.check(matches(isDisplayed()))
savedLoginsButton.check(matches(isDisplayed()))
loginExceptionsButton.check(matches(isDisplayed()))
}
fun verifyDefaultViewBeforeSyncComplete() {
@ -47,14 +53,12 @@ class SettingsSubMenuLoginsAndPasswordRobot {
mDevice.waitNotNull(Until.findObjects(By.text("On")), TestAssetHelper.waitingTime)
}
fun verifyDefaultValueExceptions() = assertDefaultValueExceptions()
fun verifyDefaultValueAutofillLogins(context: Context) = assertDefaultValueAutofillLogins(context)
fun clickAutofillOption() = autofillOption.click()
fun clickAutofillInFirefoxOption() = autofillInFirefoxOption.click()
fun verifyAutofillToggle(enabled: Boolean) =
autofillOption
fun verifyAutofillInFirefoxToggle(enabled: Boolean) {
autofillInFirefoxOption
.check(
matches(
hasCousin(
@ -69,6 +73,24 @@ class SettingsSubMenuLoginsAndPasswordRobot {
),
),
)
}
fun verifyAutofillLoginsInOtherAppsToggle(enabled: Boolean) {
autofillInOtherAppsOption
.check(
matches(
hasCousin(
allOf(
withId(R.id.switch_widget),
if (enabled) {
isChecked()
} else {
isNotChecked()
},
),
),
),
)
}
class Transition {
@ -80,32 +102,28 @@ class SettingsSubMenuLoginsAndPasswordRobot {
}
fun openSavedLogins(interact: SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.() -> Unit): SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.Transition {
fun savedLoginsButton() = onView(withText("Saved logins"))
savedLoginsButton().click()
savedLoginsButton.click()
SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot().interact()
return SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.Transition()
}
fun openLoginExceptions(interact: SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.() -> Unit): SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.Transition {
fun loginExceptionsButton() = onView(ViewMatchers.withText("Exceptions"))
loginExceptionsButton().click()
loginExceptionsButton.click()
SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot().interact()
return SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.Transition()
}
fun openSyncLogins(interact: SettingsTurnOnSyncRobot.() -> Unit): SettingsTurnOnSyncRobot.Transition {
fun syncLoginsButton() = onView(ViewMatchers.withText("Sync logins across devices"))
syncLoginsButton().click()
syncLoginsButton.click()
SettingsTurnOnSyncRobot().interact()
return SettingsTurnOnSyncRobot.Transition()
}
fun openSaveLoginsAndPasswordsOptions(interact: SettingsSubMenuLoginsAndPasswordOptionsToSaveRobot.() -> Unit): SettingsSubMenuLoginsAndPasswordOptionsToSaveRobot.Transition {
fun saveLoginsAndPasswordButton() = onView(withText("Save logins and passwords"))
saveLoginsAndPasswordButton().click()
saveLoginsAndPasswordButton.click()
SettingsSubMenuLoginsAndPasswordOptionsToSaveRobot().interact()
return SettingsSubMenuLoginsAndPasswordOptionsToSaveRobot.Transition()
@ -118,11 +136,16 @@ fun settingsSubMenuLoginsAndPassword(interact: SettingsSubMenuLoginsAndPasswordR
return SettingsSubMenuLoginsAndPasswordRobot.Transition()
}
private fun goBackButton() =
onView(CoreMatchers.allOf(ViewMatchers.withContentDescription("Navigate up")))
private val saveLoginsAndPasswordButton = onView(withText("Save logins and passwords"))
private fun assertDefaultView() = onView(ViewMatchers.withText("Sync logins across devices"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private val savedLoginsButton = onView(withText("Saved logins"))
private val syncLoginsButton = onView(withText("Sync logins across devices"))
private val loginExceptionsButton = onView(withText("Exceptions"))
private fun goBackButton() =
onView(allOf(ViewMatchers.withContentDescription("Navigate up")))
private fun assertDefaultValueAutofillLogins(context: Context) = onView(
ViewMatchers.withText(
@ -134,10 +157,6 @@ private fun assertDefaultValueAutofillLogins(context: Context) = onView(
)
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun assertDefaultValueExceptions() = onView(ViewMatchers.withText("Exceptions"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun assertDefaultValueSyncLogins() = onView(ViewMatchers.withText("Sync and save data"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private val autofillInFirefoxOption = onView(withText("Autofill in $appName"))
private val autofillOption = onView(withText("Autofill in $appName"))
private val autofillInOtherAppsOption = onView(withText("Autofill in other apps"))

@ -44,6 +44,17 @@ import org.mozilla.fenix.helpers.ext.waitNotNull
class SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot {
fun verifySecurityPromptForLogins() = assertSavedLoginsView()
fun verifyEmptySavedLoginsListView() {
onView(withText(getStringResource(R.string.preferences_passwords_saved_logins_description_empty_text)))
.check(matches(isDisplayed()))
onView(withText(R.string.preferences_passwords_saved_logins_description_empty_learn_more_link))
.check(matches(isDisplayed()))
onView(withText(R.string.preferences_logins_add_login))
.check(matches(isDisplayed()))
}
fun verifySavedLoginsAfterSync() {
mDevice.waitNotNull(
Until.findObjects(By.text("https://accounts.google.com")),

@ -0,0 +1,36 @@
/* 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.robots
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.CoreMatchers.allOf
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.click
/**
* Implementation of Robot Pattern for the Open Links In Apps sub menu.
*/
class SettingsSubMenuOpenLinksInAppsRobot {
class Transition {
fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
mDevice.waitForIdle()
goBackButton().perform(ViewActions.click())
SettingsRobot().interact()
return SettingsRobot.Transition()
}
}
}
fun clickAlwaysButton() = alwaysRadioButton().click()
private fun alwaysRadioButton() = onView(withText("Always"))
private fun goBackButton() =
onView(allOf(ViewMatchers.withContentDescription("Navigate up")))

@ -37,6 +37,7 @@ import org.hamcrest.CoreMatchers.anyOf
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.Matcher
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndDescription
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeLong
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
@ -108,6 +109,15 @@ class TabDrawerRobot {
} while (closeTabButton().exists() && retries < 3)
}
fun closeTabWithTitle(title: String) =
itemWithResIdAndDescription(
"$packageName:id/mozac_browser_tabstray_close",
"Close tab $title",
).also {
it.waitForExists(waitingTime)
it.click()
}
fun swipeTabRight(title: String) {
var retries = 0 // number of retries before failing, will stop at 2
while (!tabItem(title).waitUntilGone(waitingTimeShort) && retries < 3

@ -1,6 +1,4 @@
<manifest
package="org.mozilla.fenix"
xmlns:android="http://schemas.android.com/apk/res/android"
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.mozilla.fenix">
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@ -59,7 +58,7 @@
<activity-alias
android:name="${applicationId}.App"
android:exported="true"
android:targetActivity=".HomeActivity">
android:targetActivity=".${targetActivity}">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -141,6 +140,9 @@
</intent-filter>
</activity>
<activity android:name=".MozillaOnlineHomeActivity"
android:exported="false"/>
<activity android:name=".home.mozonline.PrivacyContentDisplayActivity"
android:exported="false"/>

@ -50,12 +50,12 @@ object FeatureFlags {
/**
* Enables the Unified Search feature.
*/
val unifiedSearchFeature = Config.channel.isNightlyOrDebug
const val unifiedSearchFeature = true
/**
* Enables compose on the tabs tray items.
*/
val composeTabsTray = Config.channel.isDebug
const val composeTabsTray = false
/**
* Enables the wallpaper v2 enhancements.
@ -71,11 +71,4 @@ object FeatureFlags {
* 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 || Config.channel.isBeta
}

@ -11,7 +11,6 @@ import android.os.Build.VERSION.SDK_INT
import android.os.StrictMode
import android.os.SystemClock
import android.util.Log.INFO
import androidx.annotation.CallSuper
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.NotificationManagerCompat
@ -87,6 +86,7 @@ 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.FenixOnboarding
import org.mozilla.fenix.onboarding.MARKETING_CHANNEL_ID
import org.mozilla.fenix.perf.MarkersActivityLifecycleCallbacks
import org.mozilla.fenix.perf.ProfilerMarkerFactProcessor
@ -124,11 +124,24 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
private set
override fun onCreate() {
super.onCreate()
if (shouldShowPrivacyNotice()) {
// For Mozilla Online build: Delay initialization on first run until privacy notice
// is accepted by the user.
return
}
initialize()
}
/**
* Initializes Fenix and all required subsystems such as Nimbus, Glean and Gecko.
*/
fun initialize() {
// We measure ourselves to avoid a call into Glean before its loaded.
val start = SystemClock.elapsedRealtimeNanos()
super.onCreate()
setupInAllProcesses()
if (!isMainProcess()) {
@ -155,6 +168,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
}
@OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
@VisibleForTesting
protected open fun initializeGlean() {
val telemetryEnabled = settings().isTelemetryEnabled
@ -195,16 +209,16 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
)
}
@CallSuper
open fun setupInAllProcesses() {
@VisibleForTesting
protected open fun setupInAllProcesses() {
setupCrashReporting()
// We want the log messages of all builds to go to Android logcat
Log.addSink(FenixLogSink(logsDebug = Config.channel.isDebug))
}
@CallSuper
open fun setupInMainProcessOnly() {
@VisibleForTesting
protected open fun setupInMainProcessOnly() {
// ⚠️ DO NOT ADD ANYTHING ABOVE THIS LINE.
// Especially references to the engine/BrowserStore which can alter the app initialization.
// See: https://github.com/mozilla-mobile/fenix/issues/26320
@ -232,14 +246,14 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
warmBrowsersCache()
initializeWebExtensionSupport()
if (FeatureFlags.storageMaintenanceFeature) {
// Make sure to call this function before registering a storage worker
// (e.g. components.core.historyStorage.registerStorageMaintenanceWorker())
// as the storage maintenance worker needs a places storage globally when
// it is needed while the app is not running and WorkManager wakes up the app
// for the periodic task.
GlobalPlacesDependencyProvider.initialize(components.core.historyStorage)
}
// Make sure to call this function before registering a storage worker
// (e.g. components.core.historyStorage.registerStorageMaintenanceWorker())
// as the storage maintenance worker needs a places storage globally when
// it is needed while the app is not running and WorkManager wakes up the app
// for the periodic task.
GlobalPlacesDependencyProvider.initialize(components.core.historyStorage)
restoreBrowserState()
restoreDownloads()
restoreMessaging()
@ -373,14 +387,12 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
}
fun queueStorageMaintenance() {
if (FeatureFlags.storageMaintenanceFeature) {
queue.runIfReadyOrQueue {
// Make sure GlobalPlacesDependencyProvider.initialize(components.core.historyStorage)
// is called before this call. When app is not running and WorkManager wakes up
// the app for the periodic task, it will require a globally provided places storage
// to run the maintenance on.
components.core.historyStorage.registerStorageMaintenanceWorker()
}
queue.runIfReadyOrQueue {
// Make sure GlobalPlacesDependencyProvider.initialize(components.core.historyStorage)
// is called before this call. When app is not running and WorkManager wakes up
// the app for the periodic task, it will require a globally provided places storage
// to run the maintenance on.
components.core.historyStorage.registerStorageMaintenanceWorker()
}
}
@ -949,4 +961,14 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
components.useCases.wallpaperUseCases.initialize()
}
}
/**
* Checks whether or not a privacy notice needs to be displayed before
* the application can continue to initialize.
*/
internal fun shouldShowPrivacyNotice(): Boolean {
return Config.channel.isMozillaOnline &&
settings().shouldShowPrivacyPopWindow &&
!FenixOnboarding(this).userHasBeenOnboarded()
}
}

@ -101,10 +101,10 @@ import org.mozilla.fenix.gleanplumb.MessageNotificationWorker
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
import org.mozilla.fenix.home.intent.OpenBrowserIntentProcessor
import org.mozilla.fenix.home.intent.OpenSpecificTabIntentProcessor
import org.mozilla.fenix.home.intent.ReEngagementIntentProcessor
import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor
import org.mozilla.fenix.home.intent.StartSearchIntentProcessor
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections
@ -113,7 +113,6 @@ 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
@ -200,7 +199,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
StartSearchIntentProcessor(),
OpenBrowserIntentProcessor(this, ::getIntentSessionId),
OpenSpecificTabIntentProcessor(this),
DefaultBrowserIntentProcessor(this),
ReEngagementIntentProcessor(this, settings()),
)
}
@ -433,7 +432,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
// that we should not rely on the application being killed between user sessions.
components.appStore.dispatch(AppAction.ResumedMetricsAction)
DefaultBrowserNotificationWorker.setDefaultBrowserNotificationIfNeeded(applicationContext)
ReEngagementNotificationWorker.setReEngagementNotificationIfNeeded(applicationContext)
MessageNotificationWorker.setMessageNotificationWorker(applicationContext)
}

@ -58,7 +58,12 @@ class IntentReceiverActivity : Activity() {
fun processIntent(intent: Intent) {
// Call process for side effects, short on the first that returns true
val private = settings().openLinksInAPrivateTab
var private = settings().openLinksInAPrivateTab
if (!private) {
// if PRIVATE_BROWSING_MODE is already set to true, honor that
private = intent.getBooleanExtra(PRIVATE_BROWSING_MODE, false)
}
intent.putExtra(PRIVATE_BROWSING_MODE, private)
if (private) {
Events.openedLink.record(Events.OpenedLinkExtra("PRIVATE"))

@ -0,0 +1,29 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import org.mozilla.fenix.home.mozonline.showPrivacyPopWindow
/**
* This activity is specific to the Mozilla Online build and used to display
* a privacy notice on first run. Once the privacy notice is accepted, and for
* all subsequent launches, it will simply launch the Fenix [HomeActivity].
*/
open class MozillaOnlineHomeActivity : AppCompatActivity() {
final override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if ((this.application as FenixApplication).shouldShowPrivacyNotice()) {
showPrivacyPopWindow(this.applicationContext, this)
} else {
startActivity(Intent(this, HomeActivity::class.java))
finish()
}
}
}

@ -17,6 +17,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityManager
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.CallSuper
import androidx.annotation.VisibleForTesting
@ -63,7 +64,8 @@ import mozilla.components.feature.contextmenu.ContextMenuCandidate
import mozilla.components.feature.contextmenu.ContextMenuFeature
import mozilla.components.feature.downloads.DownloadsFeature
import mozilla.components.feature.downloads.manager.FetchDownloadManager
import mozilla.components.feature.downloads.share.ShareDownloadFeature
import mozilla.components.feature.downloads.temporary.CopyDownloadFeature
import mozilla.components.feature.downloads.temporary.ShareDownloadFeature
import mozilla.components.feature.intent.ext.EXTRA_SESSION_ID
import mozilla.components.feature.media.fullscreen.MediaSessionFullscreenFeature
import mozilla.components.feature.privatemode.feature.SecureWindowFeature
@ -189,6 +191,7 @@ abstract class BaseBrowserFragment :
private val contextMenuFeature = ViewBoundFeatureWrapper<ContextMenuFeature>()
private val downloadsFeature = ViewBoundFeatureWrapper<DownloadsFeature>()
private val shareDownloadsFeature = ViewBoundFeatureWrapper<ShareDownloadFeature>()
private val copyDownloadsFeature = ViewBoundFeatureWrapper<CopyDownloadFeature>()
private val appLinksFeature = ViewBoundFeatureWrapper<AppLinksFeature>()
private val promptsFeature = ViewBoundFeatureWrapper<PromptFeature>()
private val findInPageIntegration = ViewBoundFeatureWrapper<FindInPageIntegration>()
@ -487,6 +490,15 @@ abstract class BaseBrowserFragment :
tabId = customTabSessionId,
)
val copyDownloadFeature = CopyDownloadFeature(
context = context.applicationContext,
httpClient = context.components.core.client,
store = store,
tabId = customTabSessionId,
snackbarParent = binding.dynamicSnackbarContainer,
snackbarDelegate = FenixSnackbarDelegate(binding.dynamicSnackbarContainer),
)
val downloadFeature = DownloadsFeature(
context.applicationContext,
store = store,
@ -595,6 +607,12 @@ abstract class BaseBrowserFragment :
view = view,
)
copyDownloadsFeature.set(
copyDownloadFeature,
owner = this,
view = view,
)
downloadsFeature.set(
downloadFeature,
owner = this,
@ -614,8 +632,9 @@ abstract class BaseBrowserFragment :
store = store,
sessionId = customTabSessionId,
fragmentManager = parentFragmentManager,
launchInApp = { context.settings().openLinksInExternalApp },
launchInApp = { context.settings().shouldOpenLinksInApp() },
loadUrlUseCase = context.components.useCases.sessionUseCases.loadUrl,
shouldPrompt = { context.settings().shouldPromptOpenLinksInApp() },
),
owner = this,
view = view,
@ -1421,12 +1440,8 @@ abstract class BaseBrowserFragment :
if (inFullScreen) {
// Close find in page bar if opened
findInPageIntegration.onBackPressed()
FenixSnackbar.make(
view = binding.dynamicSnackbarContainer,
duration = Snackbar.LENGTH_SHORT,
isDisplayedWithBrowserToolbar = false,
)
.setText(getString(R.string.full_screen_notification))
Toast
.makeText(requireContext(), R.string.full_screen_notification, Toast.LENGTH_SHORT)
.show()
activity?.enterToImmersiveMode()
(view as? SwipeGestureLayout)?.isSwipeEnabled = false

@ -8,7 +8,8 @@ import android.content.Context
import android.view.View
import mozilla.components.feature.contextmenu.ContextMenuCandidate
import mozilla.components.feature.contextmenu.ContextMenuUseCases
import mozilla.components.feature.contextmenu.DefaultSnackbarDelegate
import mozilla.components.support.utils.DefaultSnackbarDelegate
import mozilla.components.support.utils.SnackbarDelegate
class CustomTabContextMenuCandidate {
companion object {
@ -20,7 +21,7 @@ class CustomTabContextMenuCandidate {
context: Context,
contextMenuUseCases: ContextMenuUseCases,
snackBarParentView: View,
snackbarDelegate: ContextMenuCandidate.SnackbarDelegate = DefaultSnackbarDelegate(),
snackbarDelegate: SnackbarDelegate = DefaultSnackbarDelegate(),
): List<ContextMenuCandidate> = listOf(
ContextMenuCandidate.createCopyLinkCandidate(
context,

@ -6,10 +6,10 @@ package org.mozilla.fenix.browser
import android.view.View
import androidx.annotation.StringRes
import mozilla.components.feature.contextmenu.ContextMenuCandidate
import mozilla.components.support.utils.SnackbarDelegate
import org.mozilla.fenix.components.FenixSnackbar
class FenixSnackbarDelegate(private val view: View) : ContextMenuCandidate.SnackbarDelegate {
class FenixSnackbarDelegate(private val view: View) : SnackbarDelegate {
override fun show(
snackBarParentView: View,

@ -77,7 +77,7 @@ class OpenInAppOnboardingObserver(
}
private fun maybeShowOpenInAppBanner(url: String, loading: Boolean) {
if (loading || settings.openLinksInExternalApp || !settings.shouldShowOpenInAppCfr) {
if (loading || settings.shouldOpenLinksInApp() || !settings.shouldShowOpenInAppCfr) {
return
}
@ -102,7 +102,7 @@ class OpenInAppOnboardingObserver(
shouldScrollWithTopToolbar = shouldScrollWithTopToolbar,
) {
val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment(
preferenceToScrollTo = context.getString(R.string.pref_key_open_links_in_external_app),
preferenceToScrollTo = context.getString(R.string.pref_key_open_links_in_apps),
)
navController.nav(R.id.browserFragment, directions)
}

@ -429,7 +429,7 @@ class Core(
ContileTopSitesProvider(
context = context,
client = client,
maxCacheAgeInMinutes = CONTILE_MAX_CACHE_AGE,
maxCacheAgeInSeconds = CONTILE_MAX_CACHE_AGE,
)
}
@ -562,7 +562,7 @@ class Core(
private const val KEY_STORAGE_NAME = "core_prefs"
private const val RECENTLY_CLOSED_MAX = 10
const val HISTORY_METADATA_MAX_AGE_IN_MS = 14 * 24 * 60 * 60 * 1000 // 14 days
private const val CONTILE_MAX_CACHE_AGE = 60L // 60 minutes
private const val CONTILE_MAX_CACHE_AGE = 3600L // 60 minutes
const val HISTORY_SEARCH_ENGINE_ID = "history_search_engine_id"
const val BOOKMARKS_SEARCH_ENGINE_ID = "bookmarks_search_engine_id"
const val TABS_SEARCH_ENGINE_ID = "tabs_search_engine_id"

@ -5,15 +5,13 @@
package org.mozilla.fenix.components
import android.content.Context
import androidx.preference.PreferenceManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.feature.accounts.FirefoxAccountsAuthFeature
import mozilla.components.feature.app.links.AppLinksInterceptor
import mozilla.components.service.fxa.manager.FxaAccountManager
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.settings.SupportUtils
@ -37,12 +35,7 @@ class Services(
AppLinksInterceptor(
context,
interceptLinkClicks = true,
launchInApp = {
PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
context.getPreferenceKey(R.string.pref_key_open_links_in_external_app),
false,
)
},
launchInApp = { context.settings().shouldOpenLinksInApp() },
)
}
}

@ -5,7 +5,7 @@
package org.mozilla.fenix.components.metrics
enum class MetricServiceType {
Data, Marketing;
Data, Marketing
}
interface MetricsService {

@ -2,6 +2,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@file:OptIn(ExperimentalMaterialApi::class)
package org.mozilla.fenix.compose
import android.content.res.Configuration
@ -12,13 +14,11 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ChipDefaults.chipColors
import androidx.compose.material.ChipDefaults.filterChipColors
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FilterChip
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
@ -79,20 +79,16 @@ fun SelectableChip(
selectableChipColors: SelectableChipColors = SelectableChipColors.buildColors(),
onClick: () -> Unit,
) {
var selected by remember { mutableStateOf(isSelected) }
Chip(
text = text,
backgroundColor = if (selected) {
selectableChipColors.selectedBackgroundColor
} else {
selectableChipColors.unselectedBackgroundColor
},
isSquare = isSquare,
textColor = if (selected) selectableChipColors.selectedTextColor else selectableChipColors.unselectedTextColor,
FilterChip(
selected = isSelected,
onClick = onClick,
shape = if (isSquare) RoundedCornerShape(4.dp) else RoundedCornerShape(25.dp),
colors = selectableChipColors.toMaterialChipColors(),
) {
selected = !selected
onClick()
Text(
text = text,
style = FirefoxTheme.typography.body2,
)
}
}
@ -130,6 +126,17 @@ data class SelectableChipColors(
}
}
/**
* Map applications' colors for selectable chips to the platform type.
*/
@Composable
private fun SelectableChipColors.toMaterialChipColors() = filterChipColors(
selectedBackgroundColor = selectedBackgroundColor,
backgroundColor = unselectedBackgroundColor,
selectedContentColor = selectedTextColor,
contentColor = unselectedTextColor,
)
@Composable
@LightDarkPreview
private fun ChipPreview() {

@ -83,8 +83,8 @@ fun ClickableSubstringLink(
annotatedText
.getStringAnnotations("link", it, it)
.firstOrNull()?.let {
onClick()
}
onClick()
}
},
)
}

@ -21,6 +21,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.mozilla.fenix.R
@ -68,6 +69,8 @@ fun ExpandableListHeader(
color = FirefoxTheme.colors.textPrimary,
style = headerTextStyle,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f),
)
expanded?.let {

@ -84,9 +84,7 @@ fun createNimbus(context: Context, urlString: String?): NimbusApi {
*/
fun NimbusException.isReportableError(): Boolean {
return when (this) {
is NimbusException.RequestException,
is NimbusException.ResponseException,
-> false
is NimbusException.ClientException -> false
else -> true
}
}

@ -14,20 +14,17 @@ import android.os.Bundle
import android.os.IBinder
import androidx.core.app.NotificationManagerCompat
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequest
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import mozilla.components.support.base.ids.SharedIdsHelper
import org.mozilla.fenix.ext.areNotificationsEnabledSafe
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.nimbus.MessageSurfaceId
import org.mozilla.fenix.onboarding.ensureMarketingChannelExists
import org.mozilla.fenix.perf.runBlockingIncrement
import org.mozilla.fenix.utils.BootUtils
import org.mozilla.fenix.utils.IntentUtils
import org.mozilla.fenix.utils.createBaseNotification
@ -46,48 +43,48 @@ class MessageNotificationWorker(
workerParameters: WorkerParameters,
) : Worker(context, workerParameters) {
@OptIn(DelicateCoroutinesApi::class) // GlobalScope usage.
@SuppressWarnings("ReturnCount")
override fun doWork(): Result {
GlobalScope.launch(Dispatchers.IO) {
val context = applicationContext
val nm = NotificationManagerCompat.from(context)
if (!nm.areNotificationsEnabledSafe()) {
return@launch
}
val context = applicationContext
val messagingStorage = context.components.analytics.messagingStorage
val messages = messagingStorage.getMessages()
val nextMessage =
messagingStorage.getNextMessage(MessageSurfaceId.NOTIFICATION, messages)
?: return@launch
val currentBootUniqueIdentifier = BootUtils.getBootIdentifier(context)
val messageMetadata = nextMessage.metadata
// Device has NOT been power cycled.
if (messageMetadata.latestBootIdentifier == currentBootUniqueIdentifier) {
return@launch
}
val nm = NotificationManagerCompat.from(context)
if (!nm.areNotificationsEnabledSafe()) {
return Result.success()
}
val nimbusMessagingController = NimbusMessagingController(messagingStorage)
// Update message as displayed.
val updatedMessage =
nimbusMessagingController.updateMessageAsDisplayed(
nextMessage,
currentBootUniqueIdentifier,
)
nimbusMessagingController.onMessageDisplayed(updatedMessage)
nm.notify(
MESSAGE_TAG,
SharedIdsHelper.getIdForTag(context, updatedMessage.id),
buildNotification(
context,
updatedMessage,
),
)
val messagingStorage = context.components.analytics.messagingStorage
val messages = runBlockingIncrement { messagingStorage.getMessages() }
val nextMessage =
messagingStorage.getNextMessage(MessageSurfaceId.NOTIFICATION, messages)
?: return Result.success()
val currentBootUniqueIdentifier = BootUtils.getBootIdentifier(context)
val messageMetadata = nextMessage.metadata
// Device has NOT been power cycled.
if (messageMetadata.latestBootIdentifier == currentBootUniqueIdentifier) {
return Result.success()
}
val nimbusMessagingController = NimbusMessagingController(messagingStorage)
// Update message as displayed.
val updatedMessage =
nimbusMessagingController.updateMessageAsDisplayed(
nextMessage,
currentBootUniqueIdentifier,
)
runBlockingIncrement { nimbusMessagingController.onMessageDisplayed(updatedMessage) }
nm.notify(
MESSAGE_TAG,
SharedIdsHelper.getIdForTag(context, updatedMessage.id),
buildNotification(
context,
updatedMessage,
),
)
return Result.success()
}
@ -154,13 +151,10 @@ class MessageNotificationWorker(
val notificationConfig = featureConfig.notificationConfig
val pollingInterval = notificationConfig.refreshInterval.toLong()
val messageWorkRequest = PeriodicWorkRequest.Builder(
MessageNotificationWorker::class.java,
val messageWorkRequest = PeriodicWorkRequestBuilder<MessageNotificationWorker>(
pollingInterval,
TimeUnit.MINUTES,
) // Only start polling after the given interval.
.setInitialDelay(pollingInterval, TimeUnit.MINUTES)
.build()
).build()
val instanceWorkManager = WorkManager.getInstance(context)
instanceWorkManager.enqueueUniquePeriodicWork(
@ -186,21 +180,18 @@ class NotificationDismissedService : Service() {
*/
override fun onBind(intent: Intent?): IBinder? = null
@OptIn(DelicateCoroutinesApi::class) // GlobalScope usage.
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
GlobalScope.launch {
if (intent != null) {
val nimbusMessagingController =
NimbusMessagingController(applicationContext.components.analytics.messagingStorage)
// Get the relevant message.
val messageId = intent.getStringExtra(DISMISSED_MESSAGE_ID)!!
val message = nimbusMessagingController.getMessage(messageId)
if (message != null) {
// Update message as 'dismissed'.
nimbusMessagingController.onMessageDismissed(message.metadata)
}
if (intent != null) {
val nimbusMessagingController =
NimbusMessagingController(applicationContext.components.analytics.messagingStorage)
// Get the relevant message.
val messageId = intent.getStringExtra(DISMISSED_MESSAGE_ID)!!
val message = runBlockingIncrement { nimbusMessagingController.getMessage(messageId) }
if (message != null) {
// Update message as 'dismissed'.
runBlockingIncrement { nimbusMessagingController.onMessageDismissed(message.metadata) }
}
}
@ -216,30 +207,27 @@ class NotificationDismissedService : Service() {
*/
class NotificationClickedReceiverActivity : Activity() {
@OptIn(DelicateCoroutinesApi::class) // GlobalScope usage.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GlobalScope.launch {
val nimbusMessagingController =
NimbusMessagingController(components.analytics.messagingStorage)
val nimbusMessagingController =
NimbusMessagingController(components.analytics.messagingStorage)
// Get the relevant message.
val messageId = intent.getStringExtra(CLICKED_MESSAGE_ID)!!
val message = nimbusMessagingController.getMessage(messageId)
// Get the relevant message.
val messageId = intent.getStringExtra(CLICKED_MESSAGE_ID)!!
val message = runBlockingIncrement { nimbusMessagingController.getMessage(messageId) }
if (message != null) {
// Update message as 'clicked'.
nimbusMessagingController.onMessageClicked(message.metadata)
if (message != null) {
// Update message as 'clicked'.
runBlockingIncrement { nimbusMessagingController.onMessageClicked(message.metadata) }
// Create the intent.
val intent = nimbusMessagingController.getIntentForMessageAction(message.action)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
// Create the intent.
val intent = nimbusMessagingController.getIntentForMessageAction(message.action)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
// Start the message intent.
startActivity(intent)
}
// Start the message intent.
startActivity(intent)
}
// End this activity.

@ -48,7 +48,7 @@ class NimbusMessagingStorage(
@VisibleForTesting
internal val malFormedMap = mutableMapOf<String, String>()
private val logger = Logger("MessagingStorage")
private val nimbusFeature = messagingFeature.value()
private val nimbusFeature = messagingFeature
private val customAttributes: JSONObject
get() = attributeProvider?.getCustomAttributes(context) ?: JSONObject()
@ -56,11 +56,12 @@ class NimbusMessagingStorage(
* Returns a list of available messages descending sorted by their priority.
*/
suspend fun getMessages(): List<Message> {
val nimbusTriggers = nimbusFeature.triggers
val nimbusStyles = nimbusFeature.styles
val nimbusActions = nimbusFeature.actions
val featureValue = messagingFeature.value()
val nimbusTriggers = featureValue.triggers
val nimbusStyles = featureValue.styles
val nimbusActions = featureValue.actions
val nimbusMessages = nimbusFeature.messages
val nimbusMessages = featureValue.messages
val defaultStyle = StyleData()
val storageMetadata = metadataStorage.getMetadata()
@ -96,7 +97,7 @@ class NimbusMessagingStorage(
} ?: return null
// Check this isn't an experimental message. If not, we can go ahead and return it.
if (!isMessageUnderExperiment(message, nimbusFeature.messageUnderExperiment)) {
if (!isMessageUnderExperiment(message, nimbusFeature.value().messageUnderExperiment)) {
return message
}
// If the message is under experiment, then we need to record the exposure
@ -232,7 +233,7 @@ class NimbusMessagingStorage(
}
@VisibleForTesting
internal fun getOnControlBehavior(): ControlMessageBehavior = nimbusFeature.onControl
internal fun getOnControlBehavior(): ControlMessageBehavior = nimbusFeature.value().onControl
private suspend fun addMetadata(id: String): Message.Metadata {
return metadataStorage.addMetadata(

@ -4,7 +4,6 @@
package org.mozilla.fenix.gleanplumb.state
import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -68,8 +67,7 @@ class MessagingMiddleware(
next(action)
}
@VisibleForTesting
internal fun onMessagedDisplayed(
private fun onMessagedDisplayed(
oldMessage: Message,
context: AppStoreMiddlewareContext,
) {
@ -86,8 +84,7 @@ class MessagingMiddleware(
}
}
@VisibleForTesting
internal fun onMessageDismissed(
private fun onMessageDismissed(
context: AppStoreMiddlewareContext,
message: Message,
) {
@ -99,8 +96,7 @@ class MessagingMiddleware(
}
}
@VisibleForTesting
internal fun onMessageClicked(
private fun onMessageClicked(
message: Message,
context: AppStoreMiddlewareContext,
) {
@ -114,8 +110,7 @@ class MessagingMiddleware(
consumeMessageToShowIfNeeded(context, message)
}
@VisibleForTesting
internal fun consumeMessageToShowIfNeeded(
private fun consumeMessageToShowIfNeeded(
context: AppStoreMiddlewareContext,
message: Message,
) {
@ -125,16 +120,14 @@ class MessagingMiddleware(
}
}
@VisibleForTesting
internal fun removeMessage(
private fun removeMessage(
context: AppStoreMiddlewareContext,
message: Message,
): List<Message> {
return context.state.messaging.messages.filter { it.id != message.id }
}
@VisibleForTesting
internal fun updateMessage(
private fun updateMessage(
context: AppStoreMiddlewareContext,
oldMessage: Message,
updatedMessage: Message,

@ -75,11 +75,12 @@ import mozilla.components.lib.state.ext.consumeFlow
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.service.glean.private.NoExtras
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.android.content.res.resolveAttribute
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.Config
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.HomeScreen
import org.mozilla.fenix.GleanMetrics.PrivateBrowsingShortcutCfr
import org.mozilla.fenix.GleanMetrics.UnifiedSearch
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
@ -104,7 +105,6 @@ import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.gleanplumb.DefaultMessageController
import org.mozilla.fenix.gleanplumb.MessagingFeature
import org.mozilla.fenix.gleanplumb.NimbusMessagingController
import org.mozilla.fenix.home.mozonline.showPrivacyPopWindow
import org.mozilla.fenix.home.pocket.DefaultPocketStoriesController
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.recentbookmarks.RecentBookmarksFeature
@ -209,13 +209,6 @@ class HomeFragment : Fragment() {
bundleArgs = args.toBundle()
if (!onboarding.userHasBeenOnboarded() &&
requireContext().settings().shouldShowPrivacyPopWindow &&
Config.channel.isMozillaOnline
) {
showPrivacyPopWindow(requireContext(), requireActivity())
}
// DO NOT MOVE ANYTHING BELOW THIS addMarker CALL!
requireComponents.core.engine.profiler?.addMarker(
MarkersFragmentLifecycleCallbacks.MARKER_NAME,
@ -396,7 +389,6 @@ class HomeFragment : Fragment() {
recentTabController = DefaultRecentTabsController(
selectTabUseCase = components.useCases.tabsUseCases.selectTab,
navController = findNavController(),
store = components.core.store,
appStore = components.appStore,
),
recentSyncedTabController = DefaultRecentSyncedTabController(
@ -435,6 +427,8 @@ class HomeFragment : Fragment() {
appBarLayout = binding.homeAppBar
disableAppBarDragging()
activity.themeManager.applyStatusBarTheme(activity)
FxNimbus.features.homescreen.recordExposure()
@ -503,6 +497,22 @@ class HomeFragment : Fragment() {
}
}
private fun disableAppBarDragging() {
if (binding.homeAppBar.layoutParams != null) {
val appBarLayoutParams = binding.homeAppBar.layoutParams as CoordinatorLayout.LayoutParams
val appBarBehavior = AppBarLayout.Behavior()
appBarBehavior.setDragCallback(
object : AppBarLayout.Behavior.DragCallback() {
override fun canDrag(appBarLayout: AppBarLayout): Boolean {
return false
}
},
)
appBarLayoutParams.behavior = appBarBehavior
}
binding.homeAppBar.setExpanded(true)
}
private fun updateLayout(view: View) {
when (requireContext().settings().toolbarPosition) {
ToolbarPosition.TOP -> {
@ -624,7 +634,6 @@ class HomeFragment : Fragment() {
adapter.getItemViewType(it) == CollectionHeaderViewHolder.LAYOUT_ID
}
collectionPosition?.run {
appBarLayout?.setExpanded(false)
val linearLayoutManager = recyclerView.layoutManager as LinearLayoutManager
smoothScroller.targetPosition = this
linearLayoutManager.startSmoothScroll(smoothScroller)
@ -655,6 +664,11 @@ class HomeFragment : Fragment() {
text = it.name,
start = DrawableMenuIcon(
drawable = it.icon.toDrawable(resources),
tint = if (it.type == SearchEngine.Type.APPLICATION) {
requireContext().getColorFromAttr(R.attr.textPrimary)
} else {
null
},
),
) {
sessionControlInteractor.onMenuItemTapped(SearchSelectorMenu.Item.SearchEngine(it))
@ -906,12 +920,14 @@ class HomeFragment : Fragment() {
this.increaseTapArea(CFR_TAP_INCREASE_DPS)
setOnClickListener {
PrivateBrowsingShortcutCfr.addShortcut.record(NoExtras())
PrivateShortcutCreateManager.createPrivateShortcut(context)
privateBrowsingRecommend.dismiss()
}
}
layout.findViewById<Button>(R.id.cfr_neg_button).apply {
setOnClickListener {
PrivateBrowsingShortcutCfr.cancel.record()
privateBrowsingRecommend.dismiss()
}
}

@ -1,54 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.home.intent
import android.content.Intent
import androidx.navigation.NavController
import mozilla.components.concept.engine.EngineSession
import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.ext.openSetDefaultBrowserOption
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.onboarding.DefaultBrowserNotificationWorker.Companion.isDefaultBrowserNotificationIntent
import org.mozilla.fenix.onboarding.ReEngagementNotificationWorker
import org.mozilla.fenix.onboarding.ReEngagementNotificationWorker.Companion.isReEngagementNotificationIntent
/**
* When the default browser notification is tapped we need to launch [openSetDefaultBrowserOption]
*
* This should only happens once in a user's lifetime since once the user taps on the default browser
* notification, [settings.shouldShowDefaultBrowserNotification] will return false
*/
class DefaultBrowserIntentProcessor(
private val activity: HomeActivity,
) : HomeIntentProcessor {
override fun process(intent: Intent, navController: NavController, out: Intent): Boolean {
return when {
isDefaultBrowserNotificationIntent(intent) -> {
Events.defaultBrowserNotifTapped.record(NoExtras())
activity.openSetDefaultBrowserOption()
true
}
isReEngagementNotificationIntent(intent) -> {
Events.reEngagementNotifTapped.record(NoExtras())
activity.browsingModeManager.mode = BrowsingMode.Private
activity.openToBrowserAndLoad(
ReEngagementNotificationWorker.NOTIFICATION_TARGET_URL,
newTab = true,
from = BrowserDirection.FromGlobal,
flags = EngineSession.LoadUrlFlags.external(),
)
true
}
else -> false
}
}
}

@ -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.home.intent
import android.content.Intent
import androidx.navigation.NavController
import androidx.navigation.navOptions
import mozilla.components.concept.engine.EngineSession
import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.onboarding.ReEngagementNotificationWorker
import org.mozilla.fenix.onboarding.ReEngagementNotificationWorker.Companion.isReEngagementNotificationIntent
import org.mozilla.fenix.utils.Settings
/**
* Handle when the re-engagement notification is tapped
*
* This should only happens once in a user's lifetime notification,
* [settings.shouldShowReEngagementNotification] will return false if the user already seen the
* notification.
*/
class ReEngagementIntentProcessor(
private val activity: HomeActivity,
private val settings: Settings,
) : HomeIntentProcessor {
override fun process(intent: Intent, navController: NavController, out: Intent): Boolean {
return when {
isReEngagementNotificationIntent(intent) -> {
Events.reEngagementNotifTapped.record(NoExtras())
when (settings.reEngagementNotificationType) {
ReEngagementNotificationWorker.NOTIFICATION_TYPE_B -> {
val directions = NavGraphDirections.actionGlobalSearchDialog(sessionId = null)
val options = navOptions {
popUpTo(R.id.homeFragment)
}
navController.nav(null, directions, options)
}
else -> {
activity.browsingModeManager.mode = BrowsingMode.Private
activity.openToBrowserAndLoad(
ReEngagementNotificationWorker.NOTIFICATION_TARGET_URL,
newTab = true,
from = BrowserDirection.FromGlobal,
flags = EngineSession.LoadUrlFlags.external(),
)
}
}
true
}
else -> false
}
}
}

@ -5,29 +5,20 @@
package org.mozilla.fenix.home.mozonline
import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.util.AttributeSet
import android.view.View
import android.webkit.WebView
import android.widget.ImageButton
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineView
import mozilla.components.feature.contextmenu.DefaultSelectionActionDelegate
import mozilla.components.feature.search.BrowserStoreSearchAdapter
import mozilla.components.support.ktx.android.content.call
import mozilla.components.support.ktx.android.content.email
import mozilla.components.support.ktx.android.content.share
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
/**
* A special activity for displaying the detail content about privacy hyperlinked in alert dialog.
*/
class PrivacyContentDisplayActivity : Activity(), EngineSession.Observer {
private lateinit var engineView: EngineView
private lateinit var webView: WebView
private lateinit var closeButton: ImageButton
private lateinit var engineSession: EngineSession
private var url: String? = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -37,48 +28,20 @@ class PrivacyContentDisplayActivity : Activity(), EngineSession.Observer {
url = addr.getString("url")
}
engineView = findViewById<View>(R.id.privacyContentEngineView) as EngineView
webView = findViewById<View>(R.id.privacyContentEngineView) as WebView
webView.settings.javaScriptEnabled = true
closeButton = findViewById<View>(R.id.privacyContentCloseButton) as ImageButton
engineSession = components.core.engine.createSession()
}
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet,
): View? = when (name) {
EngineView::class.java.name -> components.core.engine.createView(context, attrs).apply {
selectionActionDelegate = DefaultSelectionActionDelegate(
BrowserStoreSearchAdapter(
components.core.store,
),
resources = context.resources,
shareTextClicked = { share(it) },
emailTextClicked = { email(it) },
callTextClicked = { call(it) },
)
}.asView()
else -> super.onCreateView(parent, name, context, attrs)
}
override fun onStart() {
super.onStart()
engineSession.register(this)
engineSession.let { engineSession ->
engineView.render(engineSession)
url?.let { engineSession.loadUrl(it) }
}
closeButton.setOnClickListener { finish() }
}
override fun onStop() {
super.onStop()
engineSession.unregister(this)
}
url?.let {
webView.loadUrl(it)
}
override fun onDestroy() {
super.onDestroy()
engineSession.close()
closeButton.setOnClickListener {
finish()
}
}
}

@ -6,13 +6,16 @@ package org.mozilla.fenix.home.mozonline
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.text.SpannableString
import android.text.Spanned
import android.text.method.LinkMovementMethod
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.MetricServiceType
import org.mozilla.fenix.ext.application
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import kotlin.system.exitProcess
@ -57,6 +60,10 @@ fun showPrivacyPopWindow(context: Context, activity: Activity) {
context.settings().shouldShowPrivacyPopWindow = false
context.settings().isMarketingTelemetryEnabled = true
context.components.analytics.metrics.start(MetricServiceType.Marketing)
// Now that the privacy notice is accepted, application initialization can continue.
context.application.initialize()
activity.startActivity(Intent(activity, HomeActivity::class.java))
activity.finish()
}
.setNeutralButton(
context.getString(R.string.privacy_notice_neutral_button_2),

@ -45,6 +45,9 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.semantics.testTagsAsResourceId
@ -436,13 +439,6 @@ fun PocketStoriesCategories(
categoryColors: SelectableChipColors = SelectableChipColors.buildColors(),
onCategoryClick: (PocketRecommendedStoriesCategory) -> Unit,
) {
val selectableChipColors = SelectableChipColors(
selectedTextColor = categoryColors.selectedTextColor,
unselectedTextColor = categoryColors.unselectedTextColor,
selectedBackgroundColor = categoryColors.selectedBackgroundColor,
unselectedBackgroundColor = categoryColors.unselectedBackgroundColor,
)
Box(
modifier = modifier.semantics {
testTagsAsResourceId = true
@ -458,7 +454,7 @@ fun PocketStoriesCategories(
text = category.name,
isSelected = selections.map { it.name }.contains(category.name),
isSquare = true,
selectableChipColors = selectableChipColors,
selectableChipColors = categoryColors,
) {
onCategoryClick(category)
}
@ -512,7 +508,18 @@ fun PoweredByPocketHeader(
Spacer(modifier = Modifier.width(16.dp))
Column {
val onClickLabel = stringResource(id = R.string.a11y_action_label_pocket_learn_more)
Column(
Modifier.semantics(mergeDescendants = true) {
role = Role.Button
onClick(label = onClickLabel) {
onLearnMoreClicked(
"https://www.mozilla.org/en-US/firefox/pocket/?$POCKET_FEATURE_UTM_KEY_VALUE",
)
false
}
},
) {
Text(
text = stringResource(
R.string.pocket_stories_feature_title_2,

@ -5,14 +5,12 @@
package org.mozilla.fenix.home.recenttabs.controller
import androidx.navigation.NavController
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.tabs.TabsUseCases.SelectTabUseCase
import mozilla.components.service.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.RecentTabs
import org.mozilla.fenix.R
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.ext.inProgressMediaTab
import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.home.recenttabs.interactor.RecentTabInteractor
@ -47,16 +45,11 @@ interface RecentTabController {
class DefaultRecentTabsController(
private val selectTabUseCase: SelectTabUseCase,
private val navController: NavController,
private val store: BrowserStore,
private val appStore: AppStore,
) : RecentTabController {
override fun handleRecentTabClicked(tabId: String) {
if (tabId == store.state.inProgressMediaTab?.id) {
RecentTabs.inProgressMediaTabOpened.record(NoExtras())
} else {
RecentTabs.recentTabOpened.record(NoExtras())
}
RecentTabs.recentTabOpened.record(NoExtras())
selectTabUseCase.invoke(tabId)
navController.navigate(R.id.browserFragment)

@ -566,6 +566,7 @@ class DefaultSessionControlController(
override fun handleRemoveCollectionsPlaceholder() {
settings.showCollectionsPlaceholderOnHome = false
Collections.placeholderCancel.record()
appStore.dispatch(AppAction.RemoveCollectionsPlaceholder)
}

@ -216,9 +216,9 @@ class SessionControlView(
if (!featureRecommended && !context.settings().showHomeOnboardingDialog) {
if (!context.settings().showHomeOnboardingDialog && (
context.settings().showSyncCFR ||
context.settings().shouldShowJumpBackInCFR
)
context.settings().showSyncCFR ||
context.settings().shouldShowJumpBackInCFR
)
) {
featureRecommended = HomeCFRPresenter(
context = context,

@ -4,7 +4,9 @@
package org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding
import android.content.Context
import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import org.mozilla.fenix.GleanMetrics.Onboarding
import org.mozilla.fenix.R
@ -18,6 +20,7 @@ class OnboardingTrackingProtectionViewHolder(view: View) : RecyclerView.ViewHold
private var standardTrackingProtection: OnboardingRadioButton
private var strictTrackingProtection: OnboardingRadioButton
private var descriptionText: TextView
init {
val binding = OnboardingTrackingProtectionBinding.bind(view)
@ -25,12 +28,26 @@ class OnboardingTrackingProtectionViewHolder(view: View) : RecyclerView.ViewHold
standardTrackingProtection = binding.trackingProtectionStandardOption
strictTrackingProtection = binding.trackingProtectionStrictDefault
descriptionText = binding.descriptionText
val isTCPPublic = view.context.settings().enabledTotalCookieProtectionCFR
setupDescriptionText(view.context, isTCPPublic)
val isTrackingProtectionEnabled = view.context.settings().shouldUseTrackingProtection
setupRadioGroup(isTrackingProtectionEnabled)
updateRadioGroupState(isTrackingProtectionEnabled)
}
private fun setupDescriptionText(context: Context, shuldUseNewDescription: Boolean) {
if (!shuldUseNewDescription) {
val appName = context.getString(R.string.app_name)
descriptionText.text = context.getString(
R.string.onboarding_tracking_protection_description_old,
appName,
)
}
}
private fun setupRadioGroup(isChecked: Boolean) {
updateRadioGroupState(isChecked)

@ -16,7 +16,6 @@ import mozilla.components.concept.menu.MenuController
import mozilla.components.concept.menu.Orientation
import org.mozilla.fenix.databinding.LibrarySiteItemBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.ext.loadIntoView
import org.mozilla.fenix.selection.SelectionHolder
import org.mozilla.fenix.selection.SelectionInteractor
@ -42,11 +41,6 @@ class LibrarySiteItemView @JvmOverloads constructor(
val overflowView: ImageButton get() = binding.overflowMenu
init {
overflowView.increaseTapArea(OVERFLOW_EXTRA_DIPS)
}
/**
* Change visibility of parts of this view based on what type of item is being represented.
*/
@ -103,10 +97,6 @@ class LibrarySiteItemView @JvmOverloads constructor(
}
enum class ItemType {
SITE, FOLDER;
}
companion object {
private const val OVERFLOW_EXTRA_DIPS = 16
SITE, FOLDER
}
}

@ -29,7 +29,6 @@ class BookmarkItemMenu(
OpenAllInNewTabs,
OpenAllInPrivateTabs,
Delete,
;
}
val menuController: MenuController by lazy { BrowserMenuController() }

@ -221,7 +221,7 @@ class HistoryMetadataGroupFragment :
private fun promptDeleteAll() {
if (childFragmentManager.findFragmentByTag(DeleteAllConfirmationDialogFragment.TAG)
as? DeleteAllConfirmationDialogFragment != null
as? DeleteAllConfirmationDialogFragment != null
) {
return
}

@ -1,107 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.onboarding
import android.app.Notification
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationManagerCompat
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import mozilla.components.service.glean.private.NoExtras
import mozilla.components.support.base.ids.SharedIdsHelper
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.utils.IntentUtils
import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.utils.createBaseNotification
import java.util.concurrent.TimeUnit
class DefaultBrowserNotificationWorker(
context: Context,
workerParameters: WorkerParameters,
) : Worker(context, workerParameters) {
override fun doWork(): Result {
val channelId = ensureMarketingChannelExists(applicationContext)
NotificationManagerCompat.from(applicationContext)
.notify(
NOTIFICATION_TAG,
DEFAULT_BROWSER_NOTIFICATION_ID,
buildNotification(channelId),
)
Events.defaultBrowserNotifShown.record(NoExtras())
// default browser notification should only happen once
applicationContext.settings().defaultBrowserNotificationDisplayed = true
return Result.success()
}
/**
* Build the default browser notification.
*/
private fun buildNotification(channelId: String): Notification {
val intent = Intent(applicationContext, HomeActivity::class.java)
intent.putExtra(INTENT_DEFAULT_BROWSER_NOTIFICATION, true)
val pendingIntent = PendingIntent.getActivity(
applicationContext,
SharedIdsHelper.getNextIdForTag(applicationContext, NOTIFICATION_PENDING_INTENT_TAG),
intent,
IntentUtils.defaultIntentPendingFlags,
)
with(applicationContext) {
val appName = getString(R.string.app_name)
return createBaseNotification(
this,
channelId,
getString(R.string.notification_default_browser_title, appName),
getString(R.string.notification_default_browser_text, appName),
pendingIntent,
)
}
}
companion object {
private const val NOTIFICATION_PENDING_INTENT_TAG = "org.mozilla.fenix.default.browser"
private const val INTENT_DEFAULT_BROWSER_NOTIFICATION = "org.mozilla.fenix.default.browser.intent"
private const val NOTIFICATION_TAG = "org.mozilla.fenix.default.browser.tag"
private const val NOTIFICATION_WORK_NAME = "org.mozilla.fenix.default.browser.work"
private const val NOTIFICATION_DELAY = Settings.THREE_DAYS_MS
fun isDefaultBrowserNotificationIntent(intent: Intent) =
intent.extras?.containsKey(INTENT_DEFAULT_BROWSER_NOTIFICATION) ?: false
fun setDefaultBrowserNotificationIfNeeded(context: Context) {
val instanceWorkManager = WorkManager.getInstance(context)
if (!context.settings().shouldShowDefaultBrowserNotification()) {
// cancel notification work if already default browser
instanceWorkManager.cancelUniqueWork(NOTIFICATION_WORK_NAME)
return
}
val notificationWork = OneTimeWorkRequest.Builder(DefaultBrowserNotificationWorker::class.java)
.setInitialDelay(NOTIFICATION_DELAY, TimeUnit.MILLISECONDS)
.build()
instanceWorkManager.beginUniqueWork(
NOTIFICATION_WORK_NAME,
ExistingWorkPolicy.KEEP,
notificationWork,
).enqueue()
}
}
}

@ -13,7 +13,6 @@ 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
@ -43,16 +42,14 @@ class HomeNotificationPermissionDialogFragment : DialogFragment() {
): View = ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
ProvideWindowInsets {
FirefoxTheme {
NotificationPermissionDialogScreen(
onDismiss = ::onDismiss,
grantNotificationPermission = {
ensureMarketingChannelExists(context.applicationContext)
onDismiss()
},
)
}
FirefoxTheme {
NotificationPermissionDialogScreen(
onDismiss = ::onDismiss,
grantNotificationPermission = {
ensureMarketingChannelExists(context.applicationContext)
onDismiss()
},
)
}
}
}

@ -14,13 +14,12 @@ import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.DialogFragment
import androidx.navigation.fragment.findNavController
import com.google.accompanist.insets.ProvideWindowInsets
import mozilla.components.lib.state.ext.observeAsComposableState
import org.mozilla.fenix.R
import org.mozilla.fenix.components.components
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.onboarding.view.Onboarding
import org.mozilla.fenix.onboarding.view.UpgradeOnboarding
import org.mozilla.fenix.theme.FirefoxTheme
/**
@ -47,23 +46,21 @@ class HomeOnboardingDialogFragment : DialogFragment() {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
ProvideWindowInsets {
FirefoxTheme {
val account =
components.backgroundServices.syncStore.observeAsComposableState { state -> state.account }
FirefoxTheme {
val account =
components.backgroundServices.syncStore.observeAsComposableState { state -> state.account }
Onboarding(
isSyncSignIn = account.value != null,
onDismiss = ::onDismiss,
onSignInButtonClick = {
findNavController().nav(
R.id.homeOnboardingDialogFragment,
HomeOnboardingDialogFragmentDirections.actionGlobalTurnOnSync(),
)
onDismiss()
},
)
}
UpgradeOnboarding(
isSyncSignIn = account.value != null,
onDismiss = ::onDismiss,
onSignInButtonClick = {
findNavController().nav(
R.id.homeOnboardingDialogFragment,
HomeOnboardingDialogFragmentDirections.actionGlobalTurnOnSync(),
)
onDismiss()
},
)
}
}
}

@ -15,7 +15,6 @@ import org.mozilla.fenix.R
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
const val RE_ENGAGEMENT_NOTIFICATION_ID = 2
/**

@ -78,18 +78,28 @@ class ReEngagementNotificationWorker(
)
with(applicationContext) {
return createBaseNotification(
this,
channelId,
getString(R.string.notification_re_engagement_title),
getString(R.string.notification_re_engagement_text, getString(R.string.app_name)),
pendingIntent,
)
val title = when (settings().reEngagementNotificationType) {
NOTIFICATION_TYPE_A -> getString(R.string.notification_re_engagement_A_title)
NOTIFICATION_TYPE_B -> getString(R.string.notification_re_engagement_B_title)
else -> getString(R.string.notification_re_engagement_title)
}
val text = when (settings().reEngagementNotificationType) {
NOTIFICATION_TYPE_A ->
getString(R.string.notification_re_engagement_A_text, getString(R.string.app_name))
NOTIFICATION_TYPE_B -> getString(R.string.notification_re_engagement_B_text)
else -> getString(R.string.notification_re_engagement_text, getString(R.string.app_name))
}
return createBaseNotification(this, channelId, title, text, pendingIntent)
}
}
companion object {
const val NOTIFICATION_TARGET_URL = "https://www.mozilla.org/firefox/privacy/"
const val NOTIFICATION_TYPE_A = 1
const val NOTIFICATION_TYPE_B = 2
private const val NOTIFICATION_PENDING_INTENT_TAG = "org.mozilla.fenix.re-engagement"
private const val INTENT_RE_ENGAGEMENT_NOTIFICATION = "org.mozilla.fenix.re-engagement.intent"
private const val NOTIFICATION_TAG = "org.mozilla.fenix.re-engagement.tag"

@ -4,60 +4,17 @@
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.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.statusBarsPadding
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.compose.annotation.LightDarkPreview
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.
*
@ -69,8 +26,21 @@ fun NotificationPermissionDialogScreen(
onDismiss: () -> Unit,
grantNotificationPermission: () -> Unit,
) {
NotificationPermissionContent(
notificationPermissionPageState = NotificationPageState,
OnboardingPage(
pageState = OnboardingPageState(
image = R.drawable.ic_notification_permission,
title = stringResource(
id = R.string.onboarding_home_enable_notifications_title,
formatArgs = arrayOf(stringResource(R.string.app_name)),
),
description = stringResource(
id = R.string.onboarding_home_enable_notifications_description,
formatArgs = arrayOf(stringResource(R.string.app_name)),
),
primaryButtonText = stringResource(id = R.string.onboarding_home_enable_notifications_positive_button),
secondaryButtonText = stringResource(id = R.string.onboarding_home_enable_notifications_negative_button),
onRecordImpressionEvent = { Onboarding.notifPppImpression.record(NoExtras()) },
),
onDismiss = {
onDismiss()
Onboarding.notifPppCloseClick.record(NoExtras())
@ -83,145 +53,13 @@ fun NotificationPermissionDialogScreen(
onDismiss()
Onboarding.notifPppNegativeBtnClick.record(NoExtras())
},
modifier = Modifier
.statusBarsPadding()
.navigationBarsPadding(),
)
}
@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
@LightDarkPreview
@Composable
private fun NotificationPermissionScreenPreview() {
FirefoxTheme {

@ -1,297 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.onboarding.view
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
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.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
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.telemetry.glean.private.NoExtras
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
import org.mozilla.fenix.GleanMetrics.Onboarding as OnboardingMetrics
/**
* Enum that represents the onboarding screen that is displayed.
*/
private enum class OnboardingState {
Welcome,
SyncSignIn,
}
/**
* A screen for displaying a welcome and sync sign in onboarding.
*
* @param isSyncSignIn Whether or not the user is signed into their Firefox Sync account.
* @param onDismiss Invoked when the user clicks on the close or "Skip" button.
* @param onSignInButtonClick Invoked when the user clicks on the "Sign In" button
*/
@Composable
fun Onboarding(
isSyncSignIn: Boolean,
onDismiss: () -> Unit,
onSignInButtonClick: () -> Unit,
) {
var onboardingState by remember { mutableStateOf(OnboardingState.Welcome) }
Column(
modifier = Modifier
.background(FirefoxTheme.colors.layer1)
.fillMaxSize()
.padding(bottom = 32.dp)
.statusBarsPadding()
.navigationBarsPadding()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
) {
IconButton(
onClick = {
if (onboardingState == OnboardingState.Welcome) {
OnboardingMetrics.welcomeCloseClicked.record(NoExtras())
} else {
OnboardingMetrics.syncCloseClicked.record(NoExtras())
}
onDismiss()
},
) {
Icon(
painter = painterResource(id = R.drawable.mozac_ic_close),
contentDescription = stringResource(R.string.onboarding_home_content_description_close_button),
tint = FirefoxTheme.colors.iconPrimary,
)
}
}
if (onboardingState == OnboardingState.Welcome) {
OnboardingWelcomeContent()
OnboardingWelcomeBottomContent(
onboardingState = onboardingState,
isSyncSignIn = isSyncSignIn,
onGetStartedButtonClick = {
OnboardingMetrics.welcomeGetStartedClicked.record(NoExtras())
if (isSyncSignIn) {
onDismiss()
} else {
onboardingState = OnboardingState.SyncSignIn
}
},
)
OnboardingMetrics.welcomeCardImpression.record(NoExtras())
} else if (onboardingState == OnboardingState.SyncSignIn) {
OnboardingSyncSignInContent()
OnboardingSyncSignInBottomContent(
onboardingState = onboardingState,
onSignInButtonClick = {
OnboardingMetrics.syncSignInClicked.record(NoExtras())
onSignInButtonClick()
},
onSkipButtonClick = {
OnboardingMetrics.syncSkipClicked.record(NoExtras())
onDismiss()
},
)
OnboardingMetrics.syncCardImpression.record(NoExtras())
}
}
}
@Composable
private fun OnboardingWelcomeBottomContent(
onboardingState: OnboardingState,
isSyncSignIn: Boolean,
onGetStartedButtonClick: () -> Unit,
) {
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
PrimaryButton(
text = stringResource(id = R.string.onboarding_home_get_started_button),
onClick = onGetStartedButtonClick,
)
Spacer(modifier = Modifier.height(32.dp))
if (isSyncSignIn) {
Spacer(modifier = Modifier.height(6.dp))
} else {
Indicators(onboardingState = onboardingState)
}
}
}
@Composable
private fun OnboardingWelcomeContent() {
Column(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painter = painterResource(id = R.drawable.ic_onboarding_welcome),
contentDescription = null,
)
Spacer(modifier = Modifier.height(32.dp))
Text(
text = stringResource(id = R.string.onboarding_home_welcome_title_2),
color = FirefoxTheme.colors.textPrimary,
textAlign = TextAlign.Center,
style = FirefoxTheme.typography.headline5,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(id = R.string.onboarding_home_welcome_description),
color = FirefoxTheme.colors.textSecondary,
textAlign = TextAlign.Center,
style = FirefoxTheme.typography.body2,
)
}
}
@Composable
private fun OnboardingSyncSignInContent() {
Column(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painter = painterResource(id = R.drawable.ic_onboarding_sync),
contentDescription = null,
)
Spacer(modifier = Modifier.height(32.dp))
Text(
text = stringResource(id = R.string.onboarding_home_sync_title_3),
color = FirefoxTheme.colors.textPrimary,
textAlign = TextAlign.Center,
style = FirefoxTheme.typography.headline5,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(id = R.string.onboarding_home_sync_description),
color = FirefoxTheme.colors.textSecondary,
textAlign = TextAlign.Center,
style = FirefoxTheme.typography.body2,
)
}
}
@Composable
private fun OnboardingSyncSignInBottomContent(
onboardingState: OnboardingState,
onSignInButtonClick: () -> Unit,
onSkipButtonClick: () -> Unit,
) {
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
PrimaryButton(
text = stringResource(id = R.string.onboarding_home_sign_in_button),
onClick = onSignInButtonClick,
)
Spacer(modifier = Modifier.height(8.dp))
SecondaryButton(
text = stringResource(id = R.string.onboarding_home_skip_button),
onClick = onSkipButtonClick,
)
Spacer(modifier = Modifier.height(24.dp))
Indicators(onboardingState = onboardingState)
}
}
@Composable
private fun Indicators(onboardingState: OnboardingState) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
) {
Indicator(
color = if (onboardingState == OnboardingState.Welcome) {
FirefoxTheme.colors.indicatorActive
} else {
FirefoxTheme.colors.indicatorInactive
},
)
Spacer(modifier = Modifier.width(8.dp))
Indicator(
color = if (onboardingState == OnboardingState.SyncSignIn) {
FirefoxTheme.colors.indicatorActive
} else {
FirefoxTheme.colors.indicatorInactive
},
)
}
}
@Composable
private fun Indicator(color: Color) {
Box(
modifier = Modifier
.size(6.dp)
.clip(CircleShape)
.background(color),
)
}
@Composable
@Preview
private fun OnboardingPreview() {
FirefoxTheme {
Onboarding(
isSyncSignIn = false,
onDismiss = {},
onSignInButtonClick = {},
)
}
}

@ -75,7 +75,7 @@ fun OnboardingPage(
) {
Icon(
painter = painterResource(id = R.drawable.mozac_ic_close),
contentDescription = stringResource(R.string.content_description_close_button),
contentDescription = stringResource(R.string.onboarding_home_content_description_close_button),
tint = FirefoxTheme.colors.iconPrimary,
)
}

@ -0,0 +1,215 @@
/* 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 android.os.Build
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.GleanMetrics.Onboarding as OnboardingMetrics
/**
* Enum that represents the onboarding screen that is displayed.
*/
private enum class UpgradeOnboardingState {
Welcome,
SyncSignIn,
}
/**
* A screen for displaying a welcome and sync sign in onboarding.
*
* @param isSyncSignIn Whether or not the user is signed into their Firefox Sync account.
* @param onDismiss Invoked when the user clicks on the close or "Skip" button.
* @param onSignInButtonClick Invoked when the user clicks on the "Sign In" button
*/
@Composable
fun UpgradeOnboarding(
isSyncSignIn: Boolean,
onDismiss: () -> Unit,
onSignInButtonClick: () -> Unit,
) {
CompositionLocalProvider(LocalLayoutDirection provides layoutDirection()) {
UpgradeOnboardingContent(
isSyncSignIn = isSyncSignIn,
onDismiss = onDismiss,
onSignInButtonClick = onSignInButtonClick,
)
}
}
@Composable
private fun UpgradeOnboardingContent(
isSyncSignIn: Boolean,
onDismiss: () -> Unit,
onSignInButtonClick: () -> Unit,
) {
var onboardingState by remember { mutableStateOf(UpgradeOnboardingState.Welcome) }
Column(
modifier = Modifier
.background(FirefoxTheme.colors.layer1)
.fillMaxSize()
.padding(bottom = 32.dp)
.statusBarsPadding()
.navigationBarsPadding(),
) {
OnboardingPage(
pageState = when (onboardingState) {
UpgradeOnboardingState.Welcome -> OnboardingPageState(
image = R.drawable.ic_onboarding_welcome,
title = stringResource(id = R.string.onboarding_home_welcome_title_2),
description = stringResource(id = R.string.onboarding_home_welcome_description),
primaryButtonText = stringResource(id = R.string.onboarding_home_get_started_button),
onRecordImpressionEvent = {
OnboardingMetrics.welcomeCardImpression.record(NoExtras())
},
)
UpgradeOnboardingState.SyncSignIn -> OnboardingPageState(
image = R.drawable.ic_onboarding_sync,
title = stringResource(id = R.string.onboarding_home_sync_title_3),
description = stringResource(id = R.string.onboarding_home_sync_description),
primaryButtonText = stringResource(id = R.string.onboarding_home_sign_in_button),
secondaryButtonText = stringResource(id = R.string.onboarding_home_skip_button),
onRecordImpressionEvent = {
OnboardingMetrics.syncCardImpression.record(NoExtras())
},
)
},
onDismiss = {
when (onboardingState) {
UpgradeOnboardingState.Welcome -> OnboardingMetrics.welcomeCloseClicked.record(NoExtras())
UpgradeOnboardingState.SyncSignIn -> OnboardingMetrics.syncCloseClicked.record(NoExtras())
}
onDismiss()
},
onPrimaryButtonClick = {
when (onboardingState) {
UpgradeOnboardingState.Welcome -> {
OnboardingMetrics.welcomeGetStartedClicked.record(NoExtras())
if (isSyncSignIn) {
onDismiss()
} else {
onboardingState = UpgradeOnboardingState.SyncSignIn
}
}
UpgradeOnboardingState.SyncSignIn -> {
OnboardingMetrics.syncSignInClicked.record(NoExtras())
onSignInButtonClick()
}
}
},
onSecondaryButtonClick = {
when (onboardingState) {
UpgradeOnboardingState.Welcome -> {
// Welcome does not have a secondary button.
}
UpgradeOnboardingState.SyncSignIn -> {
OnboardingMetrics.syncSkipClicked.record(NoExtras())
onDismiss()
}
}
},
modifier = Modifier.weight(1f),
)
if (isSyncSignIn) {
Spacer(modifier = Modifier.height(6.dp))
} else {
Indicators(onboardingState = onboardingState)
}
}
}
@Composable
private fun Indicators(
onboardingState: UpgradeOnboardingState,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
) {
Indicator(
color = if (onboardingState == UpgradeOnboardingState.Welcome) {
FirefoxTheme.colors.indicatorActive
} else {
FirefoxTheme.colors.indicatorInactive
},
)
Spacer(modifier = Modifier.width(8.dp))
Indicator(
color = if (onboardingState == UpgradeOnboardingState.SyncSignIn) {
FirefoxTheme.colors.indicatorActive
} else {
FirefoxTheme.colors.indicatorInactive
},
)
}
}
@Composable
private fun Indicator(color: Color) {
Box(
modifier = Modifier
.size(6.dp)
.clip(CircleShape)
.background(color),
)
}
@Composable
@LightDarkPreview
private fun OnboardingPreview() {
FirefoxTheme {
UpgradeOnboarding(
isSyncSignIn = false,
onDismiss = {},
onSignInButtonClick = {},
)
}
}
/**
* Force Left to Right layout direction when running on Android API level < 23 (Android 5.1).
* Bug with compose and RTL views causing crash in the Onboarding screen in Android 5.1.
* Bugzilla link: https://bugzilla.mozilla.org/show_bug.cgi?id=1792796
*/
@Composable
private fun layoutDirection() = if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) {
LocalLayoutDirection.current
} else {
LayoutDirection.Ltr
}

@ -171,6 +171,5 @@ class ProfilerStopDialogFragment : DialogFragment() {
enum class CardState {
UrlWarningState,
WaitForProfilerState,
;
}
}

@ -88,7 +88,6 @@ enum class ProfilerSettings(val threads: Array<String>, val features: Array<Stri
Graphics(graphics_threads, graphics_features),
Media(media_threads, media_features),
Networking(networking_threads, networking_features),
;
}
/**

@ -40,8 +40,6 @@ class StartupStateProvider(
* - if the currently started activity is not the first started activity
*/
UNKNOWN,
;
}
/**

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

Loading…
Cancel
Save