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

pull/636/head
akliuxingyuan 1 year ago
commit 75f90e48e6

@ -135,6 +135,17 @@ variants:
fileName: app-fenix-x86_64-beta-unsigned.apk
build_type: beta
name: fenixBeta
- apks:
- abi: arm64-v8a
fileName: app-fenix-arm64-v8a-benchmark-unsigned.apk
- abi: armeabi-v7a
fileName: app-fenix-armeabi-v7a-benchmark-unsigned.apk
- abi: x86
fileName: app-fenix-x86-benchmark-unsigned.apk
- abi: x86_64
fileName: app-fenix-x86_64-benchmark-unsigned.apk
build_type: benchmark
name: fenixBenchmark
- apks:
- abi: noarch
fileName: app-debug-androidTest.apk

@ -1,5 +0,0 @@
# .git-blame-ignore-revs
# For #27667 - Remove import-ordering from the list of disabled ktlint rules (#27680)
9654b4dfb122b54b04369fe80a2f9c95811478e8
# For #26844: Fix ktlint issues and remove them from baseline. (#26901)
ffcef5ff2e3f78b6972dd16551f3f653b7035ccc

@ -31,6 +31,14 @@ homescreen:
sections-enabled:
type: json
description: "This property provides a lookup table of whether or not the given section should be enabled. If the section is enabled, it should be toggleable in the settings screen, and on by default."
juno-onboarding:
description: A feature that shows juno onboarding flow.
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "if true, juno onboarding is shown to the user."
messaging:
description: "Configuration for the messaging system.\n\nIn practice this is a set of growable lookup tables for the\nmessage controller to piece together.\n"
hasExposure: true
@ -69,7 +77,7 @@ mr2022:
type: json
description: This property provides a lookup table of whether or not the given section should be enabled.
nimbus-system:
description: "Configuration of the Nimbus System in Fenix.\n"
description: "Configuration of the Nimbus System in Android.\n"
hasExposure: true
exposureDescription: ""
variables:

@ -11,8 +11,6 @@ apply plugin: 'kotlin-parcelize'
apply plugin: 'jacoco'
apply plugin: 'androidx.navigation.safeargs.kotlin'
apply plugin: 'com.google.android.gms.oss-licenses-plugin'
apply plugin: 'androidx.benchmark'
import com.android.build.OutputFile
import groovy.json.JsonOutput
@ -194,6 +192,10 @@ android {
// Use custom default allowed addon list
buildConfigField "String", "AMO_COLLECTION_USER", "\"16201230\""
buildConfigField "String", "AMO_COLLECTION_NAME", "\"What-I-want-on-Fenix\""
benchmark releaseTemplate >> {
initWith buildTypes.nightly
applicationIdSuffix ".fenix"
debuggable false
}
}
@ -497,6 +499,7 @@ nimbus {
fenixRelease: "release",
fenixForkDebug: "forkDebug",
fenixForkRelease: "forkRelease"
fenixBenchmark: "developer",
]
// This is generated by the FML and should be checked into git.
// It will be fetched by Experimenter (the Nimbus experiment website)
@ -504,33 +507,16 @@ nimbus {
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
// JUnit test runner. The runtime classpath in the built-in JUnit test runner gets the
// dependency from the `implementation`, which is type @aar, and therefore the JNA dependency
// doesn't provide the JNI dispatch libraries in the correct Java resource directories. I think
// what's happening is that @aar type in `implementation` resolves to the @jar type in
// `testImplementation`, and that it wins the dependency resolution battle.
//
// A workaround is to add a new configuration which depends on the @jar type and to reference
// the underlying JAR file directly in `testImplementation`. This JAR file doesn't resolve to
// the @aar type in `implementation`. This works when invoked via `gradle`, but also sets the
// correct runtime classpath when invoked with Android Studio's built-in JUnit test runner.
// Success!
jnaForTest
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
freeCompilerArgs += [
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=com.google.accompanist.pager.ExperimentalPagerApi",
]
}
}
dependencies {
jnaForTest FenixDependencies.jna
testImplementation files(configurations.jnaForTest.copyRecursive().files)
implementation project(':browser-engine-gecko')
implementation FenixDependencies.kotlin_coroutines
@ -635,6 +621,8 @@ dependencies {
debugImplementation FenixDependencies.leakcanary
forkDebugImplementation FenixDependencies.leakcanary
implementation FenixDependencies.androidx_activity_compose
implementation FenixDependencies.androidx_activity_ktx
implementation FenixDependencies.androidx_annotation
implementation FenixDependencies.androidx_compose_ui
implementation FenixDependencies.androidx_compose_ui_tooling
@ -652,6 +640,7 @@ dependencies {
implementation FenixDependencies.androidx_lifecycle_livedata
implementation FenixDependencies.androidx_lifecycle_process
implementation FenixDependencies.androidx_lifecycle_runtime
implementation FenixDependencies.androidx_lifecycle_viewmodel
implementation FenixDependencies.androidx_core
implementation FenixDependencies.androidx_core_ktx
@ -695,7 +684,6 @@ 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

@ -1,267 +0,0 @@
---
features:
nimbus-system:
description: |
Configuration of the Nimbus System in Fenix.
variables:
refresh-interval-foreground:
description: |
The minimum interval in minutes between fetching experiment
recipes in the foreground.
type: Int
default: 60 # 1 hour
messaging:
description: |
Configuration for the messaging system.
In practice this is a set of growable lookup tables for the
message controller to piece together.
variables:
message-under-experiment:
description: Id or prefix of the message under experiment.
type: Option<String>
default: null
messages:
description: A growable collection of messages
type: Map<String, MessageData>
default: {}
triggers:
description: >
A collection of out the box trigger
expressions. Each entry maps to a
valid JEXL expression.
type: Map<String, String>
default: {}
styles:
description: >
A map of styles to configure message
appearance.
type: Map<String, StyleData>
default: {}
actions:
type: Map<String, String>
description: A growable map of action URLs.
default: {}
on-control:
type: ControlMessageBehavior
description: What should be displayed when a control message is selected.
default: show-next-message
notification-config:
description: Configuration of the notification worker for all notification messages.
type: NotificationConfig
default: {}
defaults:
- value:
triggers:
# Using attributes built into the Nimbus SDK
USER_RECENTLY_INSTALLED: days_since_install < 7
USER_RECENTLY_UPDATED: days_since_update < 7 && days_since_install != days_since_update
USER_TIER_ONE_COUNTRY: ('US' in locale || 'GB' in locale || 'CA' in locale || 'DE' in locale || 'FR' in locale)
USER_EN_SPEAKER: "'en' in locale"
USER_ES_SPEAKER: "'es' in locale"
USER_DE_SPEAKER: "'de' in locale"
USER_FR_SPEAKER: "'fr' in locale"
DEVICE_ANDROID: os == 'Android'
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"
I_AM_NOT_DEFAULT_BROWSER: "is_default_browser == false"
USER_ESTABLISHED_INSTALL: "number_of_app_launches >=4"
FUNNEL_PAID: "adjust_campaign != ''"
FUNNEL_ORGANIC: "adjust_campaign == ''"
# Using Glean events, specific to the browser
INACTIVE_1_DAY: "'app_launched'|eventLastSeen('Hours') >= 24"
INACTIVE_2_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 2"
INACTIVE_3_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 3"
INACTIVE_4_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 4"
INACTIVE_5_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 5"
# Has the user signed in the last 4 years
FXA_SIGNED_IN: "'sync_auth.sign_in'|eventLastSeen('Years', 0) <= 4"
FXA_NOT_SIGNED_IN: "'sync_auth.sign_in'|eventLastSeen('Years', 0) > 4"
# https://mozilla-hub.atlassian.net/wiki/spaces/FJT/pages/11469471/Core+Active
USER_INFREQUENT: "'app_launched'|eventCountNonZero('Days', 28) >= 1 && 'app_launched'|eventCountNonZero('Days', 28) < 7"
USER_CASUAL: "'app_launched'|eventCountNonZero('Days', 28) >= 7 && 'app_launched'|eventCountNonZero('Days', 28) < 14"
USER_REGULAR: "'app_launched'|eventCountNonZero('Days', 28) >= 14 && 'app_launched'|eventCountNonZero('Days', 28) < 21"
USER_CORE_ACTIVE: "'app_launched'|eventCountNonZero('Days', 28) >= 21"
LAUNCHED_ONCE_THIS_WEEK: "'app_launched'|eventSum('Days', 7) == 1"
actions:
ENABLE_PRIVATE_BROWSING: ://enable_private_browsing
INSTALL_SEARCH_WIDGET: ://install_search_widget
MAKE_DEFAULT_BROWSER: ://make_default_browser
VIEW_BOOKMARKS: ://urls_bookmarks
VIEW_COLLECTIONS: ://home_collections
VIEW_HISTORY: ://urls_history
VIEW_HOMESCREEN: ://home
OPEN_SETTINGS_ACCESSIBILITY: ://settings_accessibility
OPEN_SETTINGS_ADDON_MANAGER: ://settings_addon_manager
OPEN_SETTINGS_DELETE_BROWSING_DATA: ://settings_delete_browsing_data
OPEN_SETTINGS_LOGINS: ://settings_logins
OPEN_SETTINGS_NOTIFICATIONS: ://settings_notifications
OPEN_SETTINGS_PRIVACY: ://settings_privacy
OPEN_SETTINGS_SEARCH_ENGINE: ://settings_search_engine
OPEN_SETTINGS_TRACKING_PROTECTION: ://settings_tracking_protection
OPEN_SETTINGS_WALLPAPERS: ://settings_wallpapers
OPEN_SETTINGS: ://settings
TURN_ON_SYNC: ://turn_on_sync
styles:
DEFAULT:
priority: 50
max-display-count: 5
SURVEY:
priority: 55
max-display-count: 10
PERSISTENT:
priority: 50
max-display-count: 20
WARNING:
priority: 60
max-display-count: 10
URGENT:
priority: 100
max-display-count: 10
NOTIFICATION:
priority: 50
max-display-count: 1
messages:
default-browser:
text: default_browser_experiment_card_text
surface: homescreen
action: "MAKE_DEFAULT_BROWSER"
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:
styles:
DEFAULT:
priority: 50
max-display-count: 100
EXPIRES_QUICKLY:
priority: 100
max-display-count: 1
notification-config:
refresh-interval: 120 # minutes (2 hours)
objects:
MessageData:
description: >
An object to describe a message. It uses human
readable strings to describe the triggers, action and
style of the message as well as the text of the message
and call to action.
fields:
action:
type: Text
description: >
A URL of a page or a deeplink.
This may have substitution variables in.
# This should never be defaulted.
default: empty_string
title:
type: Option<Text>
description: "The title text displayed to the user"
default: null
text:
type: Text
description: "The message text displayed to the user"
# This should never be defaulted.
default: empty_string
is-control:
type: Boolean
description: "Indicates if this message is the control message, if true shouldn't be displayed"
default: false
button-label:
type: Option<Text>
description: >
The text on the button. If no text
is present, the whole message is clickable.
default: null
style:
type: String
description: >
The style as described in a
`StyleData` from the styles table.
default: DEFAULT
surface:
description:
The surface identifier for this message.
type: MessageSurfaceId
default: homescreen
trigger:
type: List<String>
description: >
A list of strings corresponding to
targeting expressions. The message will be
shown if all expressions `true`.
default: []
StyleData:
description: >
A group of properties (predominantly visual) to
describe the style of the message.
fields:
priority:
type: Int
description: >
The importance of this message.
0 is not very important, 100 is very important.
default: 50
max-display-count:
type: Int
description: >
How many sessions will this message be shown to the user
before it is expired.
default: 5
NotificationConfig:
description: Attributes controlling the global configuration of notification messages.
fields:
refresh-interval:
type: Int
description: >
How often, in minutes, the notification message worker will wake up and check for new
messages.
default: 240 # 4 hours
enums:
ControlMessageBehavior:
description: An enum to influence what should be displayed when a control message is selected.
variants:
show-next-message:
description: The next eligible message should be shown.
show-none:
description: The surface should show no message.
MessageSurfaceId:
description: The identity of a message surface
variants:
homescreen:
description: A banner in the homescreen.
notification:
description: A local notification in the background, like a push notification.

@ -967,6 +967,361 @@ onboarding:
tags:
- Notifications
- Onboarding
set_to_default_card:
type: event
description: |
User viewed juno onboarding set to default card.
extra_keys:
element_type:
type: string
description: |
Type of element that was viewed.
action:
type: string
description: |
Type of action taken by the user.
sequence_position:
type: string
description: |
Position of the onboarding card in the onboarding flow.
sequence_id:
type: string
description: |
Identifier for the sequence.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1821726
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1554
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 126
metadata:
tags:
- Onboarding
sign_in_card:
type: event
description: |
User viewed juno onboarding sign in card.
extra_keys:
element_type:
type: string
description: |
Type of element that was viewed.
action:
type: string
description: |
Type of action taken by the user.
sequence_position:
type: string
description: |
Position of the onboarding card in the onboarding flow.
sequence_id:
type: string
description: |
Identifier for the sequence.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1821726
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1554
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 126
metadata:
tags:
- Onboarding
turn_on_notifications_card:
type: event
description: |
User viewed juno onboarding notification permission card.
extra_keys:
element_type:
type: string
description: |
Type of element that was viewed.
action:
type: string
description: |
Type of action taken by the user.
sequence_position:
type: string
description: |
Position of the onboarding card in the onboarding flow.
sequence_id:
type: string
description: |
Identifier for the sequence.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1821726
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1554
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 126
metadata:
tags:
- Onboarding
set_to_default:
type: event
description: |
User tapped on set to default button in juno onboarding.
extra_keys:
element_type:
type: string
description: |
Type of element that was viewed.
action:
type: string
description: |
Type of action taken by the user.
sequence_position:
type: string
description: |
Position of the onboarding card in the onboarding flow.
sequence_id:
type: string
description: |
Identifier for the sequence.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1821726
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1554
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 126
metadata:
tags:
- Onboarding
skip_default:
type: event
description: |
User tapped on skip set to default button in juno onboarding.
extra_keys:
element_type:
type: string
description: |
Type of element that was viewed.
action:
type: string
description: |
Type of action taken by the user.
sequence_position:
type: string
description: |
Position of the onboarding card in the onboarding flow.
sequence_id:
type: string
description: |
Identifier for the sequence.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1821726
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1554
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 126
metadata:
tags:
- Onboarding
sign_in:
type: event
description: |
User tapped on sign in button in juno onboarding.
extra_keys:
element_type:
type: string
description: |
Type of element that was viewed.
action:
type: string
description: |
Type of action taken by the user.
sequence_position:
type: string
description: |
Position of the onboarding card in the onboarding flow.
sequence_id:
type: string
description: |
Identifier for the sequence.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1821726
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1554
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 126
metadata:
tags:
- Onboarding
skip_sign_in:
type: event
description: |
User tapped on skip sign in button in juno onboarding.
extra_keys:
element_type:
type: string
description: |
Type of element that was viewed.
action:
type: string
description: |
Type of action taken by the user.
sequence_position:
type: string
description: |
Position of the onboarding card in the onboarding flow.
sequence_id:
type: string
description: |
Identifier for the sequence.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1821726
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1554
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 126
metadata:
tags:
- Onboarding
turn_on_notifications:
type: event
description: |
User tapped on turn on notifications button in juno onboarding.
extra_keys:
element_type:
type: string
description: |
Type of element that was viewed.
action:
type: string
description: |
Type of action taken by the user.
sequence_position:
type: string
description: |
Position of the onboarding card in the onboarding flow.
sequence_id:
type: string
description: |
Identifier for the sequence.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1821726
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1554
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 126
metadata:
tags:
- Onboarding
skip_turn_on_notifications:
type: event
description: |
User tapped on skip turn on notification button in juno onboarding.
extra_keys:
element_type:
type: string
description: |
Type of element that was viewed.
action:
type: string
description: |
Type of action taken by the user.
sequence_position:
type: string
description: |
Position of the onboarding card in the onboarding flow.
sequence_id:
type: string
description: |
Identifier for the sequence.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1821726
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1554
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 126
metadata:
tags:
- Onboarding
privacy_policy:
type: event
description: |
User tapped on privacy policy link in juno onboarding.
extra_keys:
element_type:
type: string
description: |
Type of element that was viewed.
action:
type: string
description: |
Type of action taken by the user.
sequence_position:
type: string
description: |
Position of the onboarding card in the onboarding flow.
sequence_id:
type: string
description: |
Identifier for the sequence.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1821726
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1554
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 126
metadata:
tags:
- Onboarding
completed:
type: event
description: |
User completed the juno onboarding.
extra_keys:
sequence_position:
type: string
description: |
Position of the onboarding card in the onboarding flow.
sequence_id:
type: string
description: |
Identifier for the sequence.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1821726
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1554
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 126
metadata:
tags:
- Onboarding
search_shortcuts:
selected:
@ -1916,6 +2271,26 @@ metrics:
metadata:
tags:
- Notifications
shared_prefs_uuid:
type: uuid
lifetime: ping
description: |
A UUID stored in Shared Preferences used to analyze technical differences
between storage mechanisms in Android, specifically the Glean DB and
Shared Preferences.
send_in_pings:
- metrics
notification_emails:
- android-probes@mozilla.com
- raphael@mozilla.com
- fbertsch@mozilla.com
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1822119
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1822119
data_sensitivity:
- technical
expires: 116
customize_home:
most_visited_sites:
@ -6440,7 +6815,26 @@ first_session:
metadata:
tags:
- Performance
adjust_attribution_time:
type: timing_distribution
time_unit: millisecond
send_in_pings:
- first-session
description: >
The time that it takes to derive the attribution parameters by
the Adjust SDK.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1823492
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1341#pullrequestreview-1354835757
data_sensitivity:
- technical
notification_emails:
- android-probes@mozilla.com
expires: 124
metadata:
tags:
- Performance
browser.search:
with_ads:
type: labeled_counter
@ -7218,6 +7612,63 @@ cookie_banners:
metadata:
tags:
- Privacy&Security
report_site_domain:
type: url
description: |
A user can report a site domain(Ex. for https://edition.cnn.com/
site domain will be cnn.com) when the cookie banner reducer is not
working from the cookie banner details panel.
lifetime: ping
send_in_pings:
- cookie-banner-report-site
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1805450
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1298#pullrequestreview-1350344223
data_sensitivity:
- technical
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 119
metadata:
tags:
- Privacy&Security
report_site_cancel_button:
type: event
description: |
The user has pressed the report site domain cancel button
from the cookie banner reducer details panel.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1805450
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1298#pullrequestreview-1350344223
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 119
metadata:
tags:
- Privacy&Security
report_domain_site_button:
type: event
description: |
The user has pressed the report site domain button
from the cookie banner reducer details panel.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1805450
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1298#pullrequestreview-1350344223
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 119
metadata:
tags:
- Privacy&Security
site_permissions:
prompt_shown:
type: event
@ -7676,6 +8127,31 @@ engine_tab:
metadata:
tags:
- Performance
tab_killed:
type: event
description: |
A tab was killed by the engine to free memory.
extra_keys:
foreground_tab:
description: |
Whether or not the tab was the currently active tab.
type: boolean
app_foreground:
description: |
Whether or not the app was in the foreground when the tab was killed.
type: boolean
had_form_data:
description: |
Whether or not the tab had unsubmitted form data when it was killed.
type: boolean
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1820211
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1343#issuecomment-1478535296
notification_emails:
- android-probes@mozilla.com
- kbrosnan@mozilla.com
expires: never
synced_tabs:
synced_tabs_suggestion_clicked:
@ -8693,106 +9169,6 @@ addresses:
tags:
- Autofill
messaging:
message_shown:
type: event
description: |
A message was shown to the user.
extra_keys:
message_key:
description: The id of the message
type: string
bugs:
- https://github.com/mozilla-mobile/fenix/issues/24224
data_reviews:
- 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: never
message_dismissed:
type: event
description: |
A message was dismissed by the user.
extra_keys:
message_key:
description: The id of the message
type: string
bugs:
- 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: never
message_clicked:
type: event
description: |
A message was clicked by the user.
extra_keys:
message_key:
description: The id of the message
type: string
action_uuid:
description: The uuid of the action
type: string
bugs:
- 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: never
message_expired:
type: event
description: |
A message maxDisplayCount has been surpassed.
extra_keys:
message_key:
description: The id of the message
type: string
bugs:
- 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: never
malformed:
type: event
description: |
A message was malformed.
extra_keys:
message_key:
description: The id of the message
type: string
bugs:
- 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: never
wallpapers:
wallpaper_settings_opened:
type: event
@ -9129,3 +9505,36 @@ private_browsing_shortcut_cfr:
notification_emails:
- android-probes@mozilla.com
expires: 122
pull_to_refresh_in_browser:
enabled:
type: boolean
description: |
Whether or not pull-to-refresh functionality
is enabled from Settings screen.
default: true
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1825413
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1775#pullrequestreview-1401966483
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 126
metadata:
tags:
- Settings
executed:
type: event
description: |
Whether the pull-to-refresh gesture was executed by the user.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1825413
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1775#pullrequestreview-1401966483
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 126

@ -9,10 +9,127 @@ channels:
- beta
- nightly
- developer
- forkDebug
- forkRelease
includes:
- messaging.fml.yaml
import:
- path: ../android-components/components/service/nimbus/messaging.fml.yaml
channel: release
features:
messaging:
- value:
triggers:
# Using attributes built into the Nimbus SDK
USER_RECENTLY_INSTALLED: days_since_install < 7
USER_RECENTLY_UPDATED: days_since_update < 7 && days_since_install != days_since_update
USER_TIER_ONE_COUNTRY: ('US' in locale || 'GB' in locale || 'CA' in locale || 'DE' in locale || 'FR' in locale)
USER_EN_SPEAKER: "'en' in locale"
USER_ES_SPEAKER: "'es' in locale"
USER_DE_SPEAKER: "'de' in locale"
USER_FR_SPEAKER: "'fr' in locale"
DEVICE_ANDROID: os == 'Android'
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
MORE_THAN_24H_SINCE_INSTALLED_OR_UPDATED: days_since_update >= 1
# Using custom attributes for the browser
I_AM_DEFAULT_BROWSER: "is_default_browser"
I_AM_NOT_DEFAULT_BROWSER: "is_default_browser == false"
USER_ESTABLISHED_INSTALL: "number_of_app_launches >=4"
FUNNEL_PAID: "adjust_campaign != ''"
FUNNEL_ORGANIC: "adjust_campaign == ''"
# Using Glean events, specific to the browser
INACTIVE_1_DAY: "'app_launched'|eventLastSeen('Hours') >= 24"
INACTIVE_2_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 2"
INACTIVE_3_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 3"
INACTIVE_4_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 4"
INACTIVE_5_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 5"
# Has the user signed in the last 4 years
FXA_SIGNED_IN: "'sync_auth.sign_in'|eventLastSeen('Years', 0) <= 4"
FXA_NOT_SIGNED_IN: "'sync_auth.sign_in'|eventLastSeen('Years', 0) > 4"
# https://mozilla-hub.atlassian.net/wiki/spaces/FJT/pages/11469471/Core+Active
USER_INFREQUENT: "'app_launched'|eventCountNonZero('Days', 28) >= 1 && 'app_launched'|eventCountNonZero('Days', 28) < 7"
USER_CASUAL: "'app_launched'|eventCountNonZero('Days', 28) >= 7 && 'app_launched'|eventCountNonZero('Days', 28) < 14"
USER_REGULAR: "'app_launched'|eventCountNonZero('Days', 28) >= 14 && 'app_launched'|eventCountNonZero('Days', 28) < 21"
USER_CORE_ACTIVE: "'app_launched'|eventCountNonZero('Days', 28) >= 21"
LAUNCHED_ONCE_THIS_WEEK: "'app_launched'|eventSum('Days', 7) == 1"
actions:
ENABLE_PRIVATE_BROWSING: ://enable_private_browsing
INSTALL_SEARCH_WIDGET: ://install_search_widget
MAKE_DEFAULT_BROWSER: ://make_default_browser
VIEW_BOOKMARKS: ://urls_bookmarks
VIEW_COLLECTIONS: ://home_collections
VIEW_HISTORY: ://urls_history
VIEW_HOMESCREEN: ://home
OPEN_SETTINGS_ACCESSIBILITY: ://settings_accessibility
OPEN_SETTINGS_ADDON_MANAGER: ://settings_addon_manager
OPEN_SETTINGS_DELETE_BROWSING_DATA: ://settings_delete_browsing_data
OPEN_SETTINGS_LOGINS: ://settings_logins
OPEN_SETTINGS_NOTIFICATIONS: ://settings_notifications
OPEN_SETTINGS_PRIVACY: ://settings_privacy
OPEN_SETTINGS_SEARCH_ENGINE: ://settings_search_engine
OPEN_SETTINGS_TRACKING_PROTECTION: ://settings_tracking_protection
OPEN_SETTINGS_WALLPAPERS: ://settings_wallpapers
OPEN_SETTINGS: ://settings
TURN_ON_SYNC: ://turn_on_sync
styles:
DEFAULT:
priority: 50
max-display-count: 5
SURVEY:
priority: 55
max-display-count: 1
PERSISTENT:
priority: 50
max-display-count: 20
WARNING:
priority: 60
max-display-count: 10
URGENT:
priority: 100
max-display-count: 10
NOTIFICATION:
priority: 50
max-display-count: 1
messages:
default-browser:
text: default_browser_experiment_card_text
surface: homescreen
action: "MAKE_DEFAULT_BROWSER"
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:
styles:
DEFAULT:
priority: 50
max-display-count: 100
EXPIRES_QUICKLY:
priority: 100
max-display-count: 1
notification-config:
refresh-interval: 120 # minutes (2 hours)
features:
homescreen:
description: The homescreen that the user goes to when they press home or new tab.
@ -136,22 +253,10 @@ features:
defaults:
- channel: nightly
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
enabled: true
- channel: developer
value:
enabled: false
enabled: true
client-deduplication:
description: A feature to control the sending of the client-deduplication ping.
@ -198,7 +303,22 @@ features:
enabled:
description: if true, the pre-permission notification prompt is shown to the user.
type: Boolean
default: true
default: false
juno-onboarding:
description: A feature that shows juno onboarding flow.
variables:
enabled:
description: if true, juno onboarding is shown to the user.
type: Boolean
default: false
defaults:
- channel: developer
value:
enabled: false
- channel: nightly
value:
enabled: true
onboarding:
description: "A feature that configures the new user onboarding page.

@ -80,3 +80,16 @@ client-deduplication:
notification_emails:
- android-probes@mozilla.com
- fbertsch@mozilla.com
cookie-banner-report-site:
description: |
This ping is needed when the cookie banner reducer doesn't work on
a website, and the user wants to report the site.
This ping doesn't include a client id.
include_client_id: false
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1805450
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1298#pullrequestreview-1350344223
notification_emails:
- android-probes@mozilla.com

@ -1,4 +1,11 @@
<html>
<!-- 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 asset is using the code behind
- https://www.mozilla-anti-tracking.com/test/dfpi/storage_access_api.html
- test page.
- Source repository: https://github.com/mozilla/anti-tracking-test-pages -->
<body>
<h2>Cross-site cookies storage access test</h2>
<h3>anti-tracker-test.com</h3>

@ -19,4 +19,8 @@
<a href="tel://1234567890">Telephone link</a>
</section>
<section>
<a href="https://m.youtube.com/user/mozilla?cbrd=1">Youtube link</a>
</section>
</html>

@ -2,14 +2,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/. -->
<!-- This asset is using the code behind
- https://www.mozilla-anti-tracking.com/test/trackingprotection/test_pages/tracking_protection.html
- test page.
- Source repository: https://github.com/mozilla/anti-tracking-test-pages -->
<html dir="ltr" xml:lang="en-US" lang="en-US">
<head>
<meta charset="utf8">
<script src="../resources/trackingAPI.js" type="text/javascript"></script>
</head>
<body>
<iframe src="http://trackertest.org/"></iframe>
<h3>Level 1 (Basic) List</h3>
<p>social-track-digest256:</p>
<img
@ -18,19 +20,19 @@
<br/>
<p>ads-track-digest256:</p>
<img
src="https://ads-track-digest256.dummytracker.org/test_not_blocked.png"
src="https://ads-track-digest256.dummytracker.org/test_not_blocked.png" alt="ads not blocked"
onerror="this.onerror=null;this.src='https://not-a-tracker.dummytracker.org/test_blocked.png';this.alt='ads blocked'">
<br/>
<p>analytics-track-digest256:</p>
<img
src="https://analytics-track-digest256.dummytracker.org/test_not_blocked.png"
src="https://analytics-track-digest256.dummytracker.org/test_not_blocked.png" alt="analytics not blocked"
onerror="this.onerror=null;this.src='https://not-a-tracker.dummytracker.org/test_blocked.png';this.alt='analytics blocked'">
<br/>
<p>Fingerprinting:
<pre id="result">test not run</pre>
<script src="https://base-fingerprinting-track-digest256.dummytracker.org/tracker.js"
onerror="this.onerror=null;var result=document.getElementById('result');result.innerHTML='Fingerprinting blocked';"
onload="this.onload=null;var result=document.getElementById('result');result.innerHTML='Fingerprinting NOT blocked';"
onload="this.onload=null;var result=document.getElementById('result');result.innerHTML='Fingerprinting not blocked';"
></script>
</p>
<br/>

@ -72,6 +72,11 @@ interface FeatureSettingsHelper {
*/
var isCookieBannerReductionDialogEnabled: Boolean
/**
* Enable or disable open in app banner.
*/
var isOpenInAppBannerEnabled: Boolean
fun applyFlagUpdates()
fun resetAllFeatureFlags()

@ -31,9 +31,11 @@ class FeatureSettingsHelperDelegate : FeatureSettingsHelper {
isRecentlyVisitedFeatureEnabled = settings.historyMetadataUIFeature,
isPWAsPromptEnabled = !settings.userKnowsAboutPwas,
isTCPCFREnabled = settings.shouldShowTotalCookieProtectionCFR,
isUnifiedSearchEnabled = false,
isWallpaperOnboardingEnabled = settings.showWallpaperOnboarding,
isDeleteSitePermissionsEnabled = settings.deleteSitePermissions,
isCookieBannerReductionDialogEnabled = !settings.userOptOutOfReEngageCookieBannerDialog,
isOpenInAppBannerEnabled = settings.shouldShowOpenInAppBanner,
etpPolicy = getETPPolicy(settings),
)
@ -60,6 +62,7 @@ class FeatureSettingsHelperDelegate : FeatureSettingsHelper {
override var isPWAsPromptEnabled: Boolean by updatedFeatureFlags::isPWAsPromptEnabled
override var isTCPCFREnabled: Boolean by updatedFeatureFlags::isTCPCFREnabled
override var isCookieBannerReductionDialogEnabled: Boolean by updatedFeatureFlags::isCookieBannerReductionDialogEnabled
override var isOpenInAppBannerEnabled: Boolean by updatedFeatureFlags::isOpenInAppBannerEnabled
override var etpPolicy: ETPPolicy by updatedFeatureFlags::etpPolicy
override fun applyFlagUpdates() {
@ -81,9 +84,11 @@ class FeatureSettingsHelperDelegate : FeatureSettingsHelper {
settings.historyMetadataUIFeature = featureFlags.isRecentlyVisitedFeatureEnabled
settings.userKnowsAboutPwas = !featureFlags.isPWAsPromptEnabled
settings.shouldShowTotalCookieProtectionCFR = featureFlags.isTCPCFREnabled
settings.showUnifiedSearchFeature = featureFlags.isUnifiedSearchEnabled
settings.showWallpaperOnboarding = featureFlags.isWallpaperOnboardingEnabled
settings.deleteSitePermissions = featureFlags.isDeleteSitePermissionsEnabled
settings.userOptOutOfReEngageCookieBannerDialog = !featureFlags.isCookieBannerReductionDialogEnabled
settings.shouldShowOpenInAppBanner = featureFlags.isOpenInAppBannerEnabled
setETPPolicy(featureFlags.etpPolicy)
}
}
@ -97,9 +102,11 @@ private data class FeatureFlags(
var isRecentlyVisitedFeatureEnabled: Boolean,
var isPWAsPromptEnabled: Boolean,
var isTCPCFREnabled: Boolean,
val isUnifiedSearchEnabled: Boolean,
var isWallpaperOnboardingEnabled: Boolean,
var isDeleteSitePermissionsEnabled: Boolean,
var isCookieBannerReductionDialogEnabled: Boolean,
var isOpenInAppBannerEnabled: Boolean,
var etpPolicy: ETPPolicy,
)

@ -49,6 +49,7 @@ class HomeActivityTestRule(
isWallpaperOnboardingEnabled: Boolean = settings.showWallpaperOnboarding,
isDeleteSitePermissionsEnabled: Boolean = settings.deleteSitePermissions,
isCookieBannerReductionDialogEnabled: Boolean = !settings.userOptOutOfReEngageCookieBannerDialog,
isOpenInAppBannerEnabled: Boolean = settings.shouldShowOpenInAppBanner,
etpPolicy: ETPPolicy = getETPPolicy(settings),
) : this(initialTouchMode, launchActivity, skipOnboarding) {
this.isHomeOnboardingDialogEnabled = isHomeOnboardingDialogEnabled
@ -61,6 +62,7 @@ class HomeActivityTestRule(
this.isWallpaperOnboardingEnabled = isWallpaperOnboardingEnabled
this.isDeleteSitePermissionsEnabled = isDeleteSitePermissionsEnabled
this.isCookieBannerReductionDialogEnabled = isCookieBannerReductionDialogEnabled
this.isOpenInAppBannerEnabled = isOpenInAppBannerEnabled
this.etpPolicy = etpPolicy
}
@ -114,6 +116,7 @@ class HomeActivityTestRule(
isTCPCFREnabled = false,
isWallpaperOnboardingEnabled = false,
isCookieBannerReductionDialogEnabled = false,
isOpenInAppBannerEnabled = false,
)
}
}
@ -150,6 +153,7 @@ class HomeActivityIntentTestRule internal constructor(
isWallpaperOnboardingEnabled: Boolean = settings.showWallpaperOnboarding,
isDeleteSitePermissionsEnabled: Boolean = settings.deleteSitePermissions,
isCookieBannerReductionDialogEnabled: Boolean = !settings.userOptOutOfReEngageCookieBannerDialog,
isOpenInAppBannerEnabled: Boolean = settings.shouldShowOpenInAppBanner,
etpPolicy: ETPPolicy = getETPPolicy(settings),
) : this(initialTouchMode, launchActivity, skipOnboarding) {
this.isHomeOnboardingDialogEnabled = isHomeOnboardingDialogEnabled
@ -162,6 +166,7 @@ class HomeActivityIntentTestRule internal constructor(
this.isWallpaperOnboardingEnabled = isWallpaperOnboardingEnabled
this.isDeleteSitePermissionsEnabled = isDeleteSitePermissionsEnabled
this.isCookieBannerReductionDialogEnabled = isCookieBannerReductionDialogEnabled
this.isOpenInAppBannerEnabled = isOpenInAppBannerEnabled
this.etpPolicy = etpPolicy
}
@ -224,6 +229,7 @@ class HomeActivityIntentTestRule internal constructor(
isWallpaperOnboardingEnabled = settings.showWallpaperOnboarding
isDeleteSitePermissionsEnabled = settings.deleteSitePermissions
isCookieBannerReductionDialogEnabled = !settings.userOptOutOfReEngageCookieBannerDialog
isOpenInAppBannerEnabled = settings.shouldShowOpenInAppBanner
etpPolicy = getETPPolicy(settings)
}
@ -251,6 +257,7 @@ class HomeActivityIntentTestRule internal constructor(
isTCPCFREnabled = false,
isWallpaperOnboardingEnabled = false,
isCookieBannerReductionDialogEnabled = false,
isOpenInAppBannerEnabled = false,
)
}
}

@ -22,6 +22,9 @@ object MatcherHelper {
fun itemContainingText(itemText: String) =
mDevice.findObject(UiSelector().textContains(itemText))
fun itemWithText(itemText: String) =
mDevice.findObject(UiSelector().text(itemText))
fun itemWithDescription(description: String) =
mDevice.findObject(UiSelector().descriptionContains(description))
@ -54,9 +57,13 @@ object MatcherHelper {
}
}
fun assertItemContainingTextExists(vararg appItems: UiObject) {
fun assertItemContainingTextExists(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))
}
}
}

@ -64,6 +64,8 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.customtabs.ExternalAppBrowserActivity
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.Constants.PackageName.YOUTUBE_APP
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
import org.mozilla.fenix.helpers.ext.waitNotNull
@ -266,17 +268,7 @@ object TestHelper {
}
}
fun assertPlayStoreOpens() {
if (isPackageInstalled(Constants.PackageName.GOOGLE_PLAY_SERVICES)) {
try {
intended(toPackage(Constants.PackageName.GOOGLE_PLAY_SERVICES))
} catch (e: AssertionFailedError) {
BrowserRobot().verifyRateOnGooglePlayURL()
}
} else {
BrowserRobot().verifyRateOnGooglePlayURL()
}
}
fun assertYoutubeAppOpens() = intended(toPackage(YOUTUBE_APP))
/**
* Checks whether the latest activity of the application is used for custom tabs or PWAs.
@ -349,17 +341,24 @@ object TestHelper {
fun grantPermission() {
if (Build.VERSION.SDK_INT >= 23) {
mDevice.findObject(
By.text(
when (Build.VERSION.SDK_INT) {
Build.VERSION_CODES.R -> Pattern.compile(
"WHILE USING THE APP",
Pattern.CASE_INSENSITIVE,
)
else -> Pattern.compile("Allow", Pattern.CASE_INSENSITIVE)
},
),
).click()
when (Build.VERSION.SDK_INT) {
Build.VERSION_CODES.R ->
itemWithResIdAndText(
"com.android.permissioncontroller:id/permission_allow_foreground_only_button",
"While using the app",
).also {
it.waitForExists(waitingTime)
it.click()
}
else ->
itemWithResIdAndText(
"com.android.packageinstaller:id/permission_allow_button",
"ALLOW",
).also {
it.waitForExists(waitingTime)
it.click()
}
}
}
}

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

@ -43,7 +43,7 @@ class BookmarksTest {
private val bookmarksFolderName = "New Folder"
private val testBookmark = object {
var title: String = "Bookmark title"
var url: String = "https://www.test.com"
var url: String = "https://www.example.com"
}
@get:Rule
@ -169,6 +169,31 @@ class BookmarksTest {
}
}
@SmokeTest
@Test
fun cancelEditBookmarkTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
}.bookmarkPage {
clickSnackbarButton("EDIT")
}
bookmarksMenu {
verifyEditBookmarksView()
changeBookmarkTitle(testBookmark.title)
changeBookmarkUrl(testBookmark.url)
}.closeEditBookmarkSection {
}
browserScreen {
}.openThreeDotMenu {
}.openBookmarks {
verifyBookmarkTitle(defaultWebPage.title)
verifyBookmarkedURL(defaultWebPage.url.toString())
}
}
@SmokeTest
@Test
fun editBookmarkTest() {
@ -177,22 +202,22 @@ class BookmarksTest {
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
) {}
}.openThreeDotMenu(defaultWebPage.url) {
}.clickEdit {
}.editBookmarkPage {
verifyEditBookmarksView()
verifyBookmarkNameEditBox()
verifyBookmarkURLEditBox()
verifyParentFolderSelector()
changeBookmarkTitle(testBookmark.title)
changeBookmarkUrl(testBookmark.url)
saveEditBookmark()
}
browserScreen {
}.openThreeDotMenu {
}.openBookmarks {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
) {}
verifyBookmarkTitle(testBookmark.title)
verifyBookmarkedURL(testBookmark.url)
verifyKeyboardHidden()
}.openBookmarkWithTitle(testBookmark.title) {
verifyUrl("example.com")
}
}

@ -18,7 +18,6 @@ import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.RetryTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.ui.robots.downloadRobot
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
/**
@ -76,7 +75,7 @@ class ContextMenusTest {
verifyLinkContextMenuItems(genericURL.url)
clickContextOpenLinkInNewTab()
verifySnackBarText("New tab opened")
snackBarButtonClick()
clickSnackbarButton("SWITCH")
verifyUrl(genericURL.url.toString())
}.openTabDrawer {
verifyNormalModeSelected()
@ -100,7 +99,7 @@ class ContextMenusTest {
verifyLinkContextMenuItems(genericURL.url)
clickContextOpenLinkInPrivateTab()
verifySnackBarText("New private tab opened")
snackBarButtonClick()
clickSnackbarButton("SWITCH")
verifyUrl(genericURL.url.toString())
}.openTabDrawer {
verifyPrivateModeSelected()
@ -180,7 +179,7 @@ class ContextMenusTest {
verifyLinkImageContextMenuItems(imageResource.url)
clickContextOpenImageNewTab()
verifySnackBarText("New tab opened")
snackBarButtonClick()
clickSnackbarButton("SWITCH")
verifyUrl(imageResource.url.toString())
}
}
@ -250,48 +249,4 @@ class ContextMenusTest {
verifyNoLinkImageContextMenuItems(imageResource.url)
}
}
@SmokeTest
@Test
fun shareSelectedTextTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
longClickLink(genericURL.content)
}.clickShareSelectedText {
verifyAndroidShareLayout()
}
}
@SmokeTest
@Test
fun selectAndSearchTextTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
longClickAndSearchText("Search", "content")
mDevice.waitForIdle()
verifyTabCounter("2")
verifyUrl("google")
}
}
@SmokeTest
@Test
fun privateSelectAndSearchTextTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.togglePrivateBrowsingMode()
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
longClickAndSearchText("Private Search", "content")
mDevice.waitForIdle()
verifyTabCounter("2")
verifyUrl("google")
}
}
}

@ -9,6 +9,7 @@ import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
@ -51,7 +52,7 @@ class ContextualHintsTest {
navigationToolbar {
}.enterURLAndEnterToBrowser(genericPage.url) {
verifyCookiesProtectionHint()
verifyCookiesProtectionHintIsDisplayed(true)
// One back press to dismiss the TCP hint
mDevice.pressBack()
}.goToHomescreen {
@ -59,13 +60,29 @@ class ContextualHintsTest {
}
}
@SmokeTest
@Test
fun cookieProtectionHintTest() {
fun openTotalCookieProtectionLearnMoreLinkTest() {
val genericPage = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericPage.url) {
verifyCookiesProtectionHint()
verifyCookiesProtectionHintIsDisplayed(true)
clickTotalCookieProtectionLearnMoreLink()
verifyUrl("support.mozilla.org/en-US/kb/enhanced-tracking-protection-firefox-android")
}
}
@SmokeTest
@Test
fun dismissTotalCookieProtectionHintTest() {
val genericPage = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericPage.url) {
verifyCookiesProtectionHintIsDisplayed(true)
clickTotalCookieProtectionCloseButton()
verifyCookiesProtectionHintIsDisplayed(false)
}
}
}

@ -5,7 +5,6 @@ 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
@ -28,10 +27,13 @@ class CookieBannerReductionTest {
verifyCookieBannerExists(exists = true)
}.openThreeDotMenu {
}.openSettings {
verifySettingsOptionSummary("Cookie Banner Reduction", "Off")
}.openCookieBannerReductionSubMenu {
verifyCookieBannerView(isCookieBannerReductionChecked = false)
clickCookieBannerReductionToggle()
verifyCheckedCookieBannerReductionToggle(isCookieBannerReductionChecked = true)
}.goBack {
verifySettingsOptionSummary("Cookie Banner Reduction", "On")
}
exitMenu()
@ -40,7 +42,7 @@ class CookieBannerReductionTest {
verifyCookieBannerExists(exists = false)
}
TestHelper.restartApp(activityTestRule)
restartApp(activityTestRule)
browserScreen {
verifyCookieBannerExists(exists = false)
@ -74,12 +76,17 @@ class CookieBannerReductionTest {
verifyCookieBannerExists(exists = true)
}.openThreeDotMenu {
}.openSettings {
verifySettingsOptionSummary("Cookie Banner Reduction", "Off")
}.openCookieBannerReductionSubMenu {
verifyCookieBannerView(isCookieBannerReductionChecked = false)
clickCookieBannerReductionToggle()
verifyCheckedCookieBannerReductionToggle(isCookieBannerReductionChecked = true)
exitMenu()
}.goBack {
verifySettingsOptionSummary("Cookie Banner Reduction", "On")
}
exitMenu()
browserScreen {
verifyCookieBannerExists(exists = false)
}

@ -123,7 +123,7 @@ class DeepLinkTest {
@Test
fun openSettingsTrackingProtection() {
robot.openSettingsTrackingProtection {
verifyEnhancedTrackingProtectionHeader()
verifyEnhancedTrackingProtectionSummary()
}
}

@ -5,6 +5,7 @@
package org.mozilla.fenix.ui
import androidx.core.net.toUri
import androidx.test.espresso.Espresso.pressBack
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
@ -13,14 +14,18 @@ import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestAssetHelper.getEnhancedTrackingProtectionAsset
import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
import org.mozilla.fenix.helpers.TestHelper.appContext
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.enhancedTrackingProtection
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.settingsSubMenuEnhancedTrackingProtection
/**
* Tests for verifying basic UI functionality of Enhanced Tracking Protection
@ -28,10 +33,10 @@ import org.mozilla.fenix.ui.robots.settingsSubMenuEnhancedTrackingProtection
* Including
* - Verifying default states
* - Verifying Enhanced Tracking Protection notification bubble
* - Verifying Enhanced Tracking Protection notification shield
* - Verifying Enhanced Tracking Protection content sheet
* - Verifying Enhanced Tracking Protection content sheet details
* - Verifying Enhanced Tracking Protection toggle
* - Verifying Enhanced Tracking Protection options and functionality
* - Verifying Enhanced Tracking Protection site exceptions
*/
@ -39,7 +44,7 @@ class EnhancedTrackingProtectionTest {
private lateinit var mockWebServer: MockWebServer
@get:Rule
val activityTestRule = HomeActivityTestRule(
val activityTestRule = HomeActivityIntentTestRule(
isJumpBackInCFREnabled = false,
isTCPCFREnabled = false,
isWallpaperOnboardingEnabled = false,
@ -59,25 +64,64 @@ class EnhancedTrackingProtectionTest {
}
@Test
fun testSettingsDefaults() {
fun testETPSettingsItemsAndSubMenus() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyEnhancedTrackingProtectionButton()
verifyEnhancedTrackingProtectionState("Standard")
verifySettingsOptionSummary("Enhanced Tracking Protection", "Standard")
}.openEnhancedTrackingProtectionSubMenu {
verifyEnhancedTrackingProtectionHeader()
verifyEnhancedTrackingProtectionOptionsEnabled()
verifyEnhancedTrackingProtectionSummary()
verifyLearnMoreText()
verifyEnhancedTrackingProtectionTextWithSwitchWidget()
verifyTrackingProtectionSwitchEnabled()
verifyEnhancedTrackingProtectionOptionsEnabled()
verifyEnhancedTrackingProtectionLevelSelected("Standard (default)", true)
verifyStandardOptionDescription()
verifyStrictOptionDescription()
selectTrackingProtectionOption("Custom")
verifyCustomTrackingProtectionSettings()
scrollToElementByText("Standard (default)")
verifyWhatsBlockedByStandardETPInfo()
pressBack()
verifyWhatsBlockedByStrictETPInfo()
pressBack()
verifyWhatsBlockedByCustomETPInfo()
pressBack()
}.openExceptions {
verifyDefault()
verifyTPExceptionsDefaultView()
openExceptionsLearnMoreLink()
}
browserScreen {
verifyUrl("support.mozilla.org/en-US/kb/enhanced-tracking-protection-firefox-android")
}
}
@Test
fun testETPSettingsSummaryChange() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyEnhancedTrackingProtectionButton()
verifySettingsOptionSummary("Enhanced Tracking Protection", "Standard")
}.openEnhancedTrackingProtectionSubMenu {
selectTrackingProtectionOption("Strict")
}.goBack {
verifySettingsOptionSummary("Enhanced Tracking Protection", "Strict")
}.openEnhancedTrackingProtectionSubMenu {
selectTrackingProtectionOption("Custom")
}.goBack {
verifySettingsOptionSummary("Enhanced Tracking Protection", "Custom")
}.openEnhancedTrackingProtectionSubMenu {
switchEnhancedTrackingProtectionToggle()
}.goBack {
verifySettingsOptionSummary("Enhanced Tracking Protection", "Off")
}
}
@SmokeTest
@Test
fun testETPOffGlobally() {
val genericPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val genericPage = getGenericAsset(mockWebServer, 1)
homeScreen {
}.openThreeDotMenu {
@ -107,69 +151,140 @@ class EnhancedTrackingProtectionTest {
}
}
// Tests adding ETP exceptions to websites and keeping that preference after restart
@SmokeTest
@Test
fun testStrictVisitProtectionSheet() {
appContext.settings().setStrictETP()
val genericPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val trackingProtectionTest =
TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer)
fun testDisableETPExceptionToggle() {
val firstPage = getGenericAsset(mockWebServer, 1)
val secondPage = "example.com"
// browsing a generic page to allow GV to load on a fresh run
navigationToolbar {
}.enterURLAndEnterToBrowser(genericPage.url) {
}.openTabDrawer {
closeTab()
}.enterURLAndEnterToBrowser(firstPage.url) {}
enhancedTrackingProtection {
}.openEnhancedTrackingProtectionSheet {
}.toggleEnhancedTrackingProtectionFromSheet {
verifyEnhancedTrackingProtectionSheetStatus("OFF", false)
}.closeEnhancedTrackingProtectionSheet {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(secondPage.toUri()) {
verifyPageContent("Example Domain")
}
navigationToolbar {
}.enterURLAndEnterToBrowser(trackingProtectionTest.url) {}
enhancedTrackingProtection {
}.openEnhancedTrackingProtectionSheet {
verifyEnhancedTrackingProtectionSheetStatus("ON", true)
}.toggleEnhancedTrackingProtectionFromSheet {
verifyEnhancedTrackingProtectionSheetStatus("OFF", false)
}
restartApp(activityTestRule)
enhancedTrackingProtection {
}.openEnhancedTrackingProtectionSheet {
verifyEnhancedTrackingProtectionSheetStatus("OFF", false)
}
}
@Test
fun testStrictVisitDisableExceptionToggle() {
appContext.settings().setStrictETP()
val genericPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val trackingProtectionTest =
TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer)
fun trackingProtectionSwitchEnabledRemovesExceptionTest() {
val trackingPage = getEnhancedTrackingProtectionAsset(mockWebServer)
// browsing a generic page to allow GV to load on a fresh run
navigationToolbar {
}.enterURLAndEnterToBrowser(genericPage.url) {
}.openTabDrawer {
closeTab()
}.enterURLAndEnterToBrowser(trackingPage.url) {
waitForPageToLoad()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(trackingProtectionTest.url) {}
enhancedTrackingProtection {
}.openEnhancedTrackingProtectionSheet {
verifyEnhancedTrackingProtectionSheetStatus("ON", true)
}.disableEnhancedTrackingProtectionFromSheet {
}.toggleEnhancedTrackingProtectionFromSheet {
verifyEnhancedTrackingProtectionSheetStatus("OFF", false)
}.closeEnhancedTrackingProtectionSheet {
}.openThreeDotMenu {
}.openSettings {
}.openEnhancedTrackingProtectionSubMenu {
}.openExceptions {
verifySiteExceptionExists(trackingPage.url.host.toString(), true)
exitMenu()
}
enhancedTrackingProtection {
}.openEnhancedTrackingProtectionSheet {
}.toggleEnhancedTrackingProtectionFromSheet {
verifyEnhancedTrackingProtectionSheetStatus("ON", true)
}.openProtectionSettings {
verifyEnhancedTrackingProtectionHeader()
verifyEnhancedTrackingProtectionOptionsEnabled()
verifyTrackingProtectionSwitchEnabled()
}.openExceptions {
verifySiteExceptionExists(trackingPage.url.host.toString(), false)
}
}
settingsSubMenuEnhancedTrackingProtection {
// Tests removing TP exceptions individually or all at once
@Test
fun clearTrackingProtectionExceptionsTest() {
val firstPage = getGenericAsset(mockWebServer, 1)
val secondPage = "example.com"
navigationToolbar {
}.enterURLAndEnterToBrowser(firstPage.url) {}
enhancedTrackingProtection {
}.openEnhancedTrackingProtectionSheet {
}.toggleEnhancedTrackingProtectionFromSheet {
verifyEnhancedTrackingProtectionSheetStatus("OFF", false)
}.closeEnhancedTrackingProtectionSheet {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(secondPage.toUri()) {
verifyPageContent("Example Domain")
}
enhancedTrackingProtection {
}.openEnhancedTrackingProtectionSheet {
}.toggleEnhancedTrackingProtectionFromSheet {
verifyEnhancedTrackingProtectionSheetStatus("OFF", false)
}.closeEnhancedTrackingProtectionSheet {
}.openThreeDotMenu {
}.openSettings {
}.openEnhancedTrackingProtectionSubMenu {
}.openExceptions {
verifyListedURL(trackingProtectionTest.url.host.toString())
removeOneSiteException(secondPage)
}.disableExceptions {
verifyDefault()
verifyTPExceptionsDefaultView()
exitMenu()
}
enhancedTrackingProtection {
}.openEnhancedTrackingProtectionSheet {
verifyEnhancedTrackingProtectionSheetStatus("ON", true)
}
}
@Test
fun testStandardETPVisitSheetDetails() {
val genericPage = getGenericAsset(mockWebServer, 1)
val trackingProtectionTest = getEnhancedTrackingProtectionAsset(mockWebServer).url
// browsing a generic page to allow GV to load on a fresh run
navigationToolbar {
}.enterURLAndEnterToBrowser(genericPage.url) {
verifyPageContent(genericPage.content)
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(trackingProtectionTest) {
verifyTrackingProtectionWebContent("social not blocked")
verifyTrackingProtectionWebContent("ads not blocked")
verifyTrackingProtectionWebContent("analytics not blocked")
verifyTrackingProtectionWebContent("Fingerprinting blocked")
verifyTrackingProtectionWebContent("Cryptomining blocked")
}
enhancedTrackingProtection {
}.openEnhancedTrackingProtectionSheet {
verifyEnhancedTrackingProtectionSheetStatus("ON", true)
}.openDetails {
verifyCrossSiteCookiesBlocked(true)
navigateBackToDetails()
verifyCryptominersBlocked(true)
navigateBackToDetails()
verifyFingerprintersBlocked(true)
navigateBackToDetails()
verifyTrackingContentBlocked(false)
}.closeEnhancedTrackingProtectionSheet {}
}
@Test
fun testStrictVisitSheetDetails() {
appContext.settings().setStrictETP()
val genericPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val trackingProtectionTest =
TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer)
val genericPage = getGenericAsset(mockWebServer, 1)
val trackingProtectionTest = getEnhancedTrackingProtectionAsset(mockWebServer).url
// browsing a generic page to allow GV to load on a fresh run
navigationToolbar {
@ -179,7 +294,7 @@ class EnhancedTrackingProtectionTest {
}
navigationToolbar {
}.enterURLAndEnterToBrowser(trackingProtectionTest.url) {
}.enterURLAndEnterToBrowser(trackingProtectionTest) {
verifyTrackingProtectionWebContent("social blocked")
verifyTrackingProtectionWebContent("ads blocked")
verifyTrackingProtectionWebContent("analytics blocked")
@ -190,31 +305,31 @@ class EnhancedTrackingProtectionTest {
}.openEnhancedTrackingProtectionSheet {
verifyEnhancedTrackingProtectionSheetStatus("ON", true)
}.openDetails {
verifyEnhancedTrackingProtectionDetailsStatus("Blocked")
verifyTrackingCookiesBlocked()
verifyCryptominersBlocked()
verifyFingerprintersBlocked()
verifyTrackingContentBlocked()
verifySocialMediaTrackersBlocked(true)
navigateBackToDetails()
verifyCryptominersBlocked(true)
navigateBackToDetails()
verifyFingerprintersBlocked(true)
navigateBackToDetails()
verifyTrackingContentBlocked(true)
viewTrackingContentBlockList()
}
}
@SmokeTest
@Test
fun customTrackingProtectionSettingsTest() {
val genericWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val trackingPage = TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer)
fun defaultCustomTrackingProtectionSettingsTest() {
val genericWebPage = getGenericAsset(mockWebServer, 1)
val trackingPage = getEnhancedTrackingProtectionAsset(mockWebServer)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openEnhancedTrackingProtectionSubMenu {
verifyEnhancedTrackingProtectionOptionsEnabled()
selectTrackingProtectionOption("Custom")
verifyCustomTrackingProtectionSettings()
}.goBackToHomeScreen {}
navigationToolbar {
}.goBackToHomeScreen {
}.openNavigationToolbar {
// browsing a basic page to allow GV to load on a fresh run
}.enterURLAndEnterToBrowser(genericWebPage.url) {
}.openNavigationToolbar {
@ -229,10 +344,138 @@ class EnhancedTrackingProtectionTest {
enhancedTrackingProtection {
}.openEnhancedTrackingProtectionSheet {
}.openDetails {
verifyTrackingCookiesBlocked()
verifyCryptominersBlocked()
verifyFingerprintersBlocked()
verifyTrackingContentBlocked()
verifyCrossSiteCookiesBlocked(true)
navigateBackToDetails()
verifyCryptominersBlocked(true)
navigateBackToDetails()
verifyFingerprintersBlocked(true)
navigateBackToDetails()
verifyTrackingContentBlocked(true)
viewTrackingContentBlockList()
}
}
// Tests the trackers blocked with the following Custom TP set up:
// - Cookies set to "All cookies"
// - Tracking content option OFF
// - Fingerprinters, cryptominers and redirect trackers checked
@Test
fun customizedTrackingProtectionOptionsTest() {
val genericWebPage = getGenericAsset(mockWebServer, 1)
val trackingPage = getEnhancedTrackingProtectionAsset(mockWebServer)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openEnhancedTrackingProtectionSubMenu {
selectTrackingProtectionOption("Custom")
verifyCustomTrackingProtectionSettings()
selectTrackingProtectionOption("Isolate cross-site cookies")
selectTrackingProtectionOption("All cookies (will cause websites to break)")
selectTrackingProtectionOption("Tracking content")
}.goBackToHomeScreen {
mDevice.waitForIdle()
}.openNavigationToolbar {
// browsing a basic page to allow GV to load on a fresh run
}.enterURLAndEnterToBrowser(genericWebPage.url) {
waitForPageToLoad()
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(trackingPage.url) {
verifyTrackingProtectionWebContent("social not blocked")
verifyTrackingProtectionWebContent("ads not blocked")
verifyTrackingProtectionWebContent("analytics not blocked")
}
enhancedTrackingProtection {
}.openEnhancedTrackingProtectionSheet {
}.openDetails {
verifyCrossSiteCookiesBlocked(true)
navigateBackToDetails()
verifyCryptominersBlocked(true)
navigateBackToDetails()
verifyFingerprintersBlocked(true)
navigateBackToDetails()
verifyTrackingContentBlocked(false)
}
}
@Test
fun disableCustomTrackingProtectionOptionsTest() {
val genericWebPage = getGenericAsset(mockWebServer, 1)
val trackingPage = getEnhancedTrackingProtectionAsset(mockWebServer)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openEnhancedTrackingProtectionSubMenu {
selectTrackingProtectionOption("Custom")
verifyCustomTrackingProtectionSettings()
selectTrackingProtectionOption("Cookies")
selectTrackingProtectionOption("Tracking content")
selectTrackingProtectionOption("Cryptominers")
selectTrackingProtectionOption("Fingerprinters")
selectTrackingProtectionOption("Redirect Trackers")
}.goBackToHomeScreen {
mDevice.waitForIdle()
}.openNavigationToolbar {
// browsing a basic page to allow GV to load on a fresh run
}.enterURLAndEnterToBrowser(genericWebPage.url) {
waitForPageToLoad()
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(trackingPage.url) {
verifyTrackingProtectionWebContent("social not blocked")
verifyTrackingProtectionWebContent("ads not blocked")
verifyTrackingProtectionWebContent("analytics not blocked")
verifyTrackingProtectionWebContent("Fingerprinting not blocked")
verifyTrackingProtectionWebContent("Cryptomining not blocked")
}
}
@Test
fun testTrackingContentBlockedOnlyInPrivateTabs() {
val genericWebPage = getGenericAsset(mockWebServer, 1)
val trackingPage = getEnhancedTrackingProtectionAsset(mockWebServer)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openEnhancedTrackingProtectionSubMenu {
verifyEnhancedTrackingProtectionOptionsEnabled()
selectTrackingProtectionOption("Custom")
verifyCustomTrackingProtectionSettings()
selectTrackingProtectionOption("In all tabs")
selectTrackingProtectionOption("Only in Private tabs")
}.goBackToHomeScreen {
}.openNavigationToolbar {
// browsing a basic page to allow GV to load on a fresh run
}.enterURLAndEnterToBrowser(genericWebPage.url) {
waitForPageToLoad()
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(trackingPage.url) {
verifyTrackingProtectionWebContent("social not blocked")
verifyTrackingProtectionWebContent("ads not blocked")
verifyTrackingProtectionWebContent("analytics not blocked")
verifyTrackingProtectionWebContent("Fingerprinting blocked")
verifyTrackingProtectionWebContent("Cryptomining blocked")
}.goToHomescreen {
}.togglePrivateBrowsingMode()
navigationToolbar {
}.enterURLAndEnterToBrowser(trackingPage.url) {
verifyTrackingProtectionWebContent("social blocked")
verifyTrackingProtectionWebContent("ads blocked")
verifyTrackingProtectionWebContent("analytics blocked")
verifyTrackingProtectionWebContent("Fingerprinting blocked")
verifyTrackingProtectionWebContent("Cryptomining blocked")
}
enhancedTrackingProtection {
}.openEnhancedTrackingProtectionSheet {
}.openDetails {
verifyCrossSiteCookiesBlocked(true)
navigateBackToDetails()
verifyCryptominersBlocked(true)
navigateBackToDetails()
verifyFingerprintersBlocked(true)
navigateBackToDetails()
verifyTrackingContentBlocked(true)
viewTrackingContentBlockList()
}
}
@ -241,12 +484,17 @@ class EnhancedTrackingProtectionTest {
@Test
fun blockCookiesStorageAccessTest() {
// With Standard TrackingProtection settings
val page = mockWebServer.url("pages/cross-site-cookies.html").toString().toUri()
val genericWebPage = getGenericAsset(mockWebServer, 1)
val testPage = mockWebServer.url("pages/cross-site-cookies.html").toString().toUri()
val originSite = "https://mozilla-mobile.github.io"
val currentSite = "http://localhost:${mockWebServer.port}"
navigationToolbar {
}.enterURLAndEnterToBrowser(page) {
}.enterURLAndEnterToBrowser(genericWebPage.url) {
waitForPageToLoad()
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(testPage) {
waitForPageToLoad()
}.clickRequestStorageAccessButton {
verifyCrossOriginCookiesPermissionPrompt(originSite, currentSite)
}.clickPagePermissionButton(allow = false) {
@ -258,12 +506,17 @@ class EnhancedTrackingProtectionTest {
@Test
fun allowCookiesStorageAccessTest() {
// With Standard TrackingProtection settings
val page = mockWebServer.url("pages/cross-site-cookies.html").toString().toUri()
val genericWebPage = getGenericAsset(mockWebServer, 1)
val testPage = mockWebServer.url("pages/cross-site-cookies.html").toString().toUri()
val originSite = "https://mozilla-mobile.github.io"
val currentSite = "http://localhost:${mockWebServer.port}"
navigationToolbar {
}.enterURLAndEnterToBrowser(page) {
}.enterURLAndEnterToBrowser(genericWebPage.url) {
waitForPageToLoad()
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(testPage) {
waitForPageToLoad()
}.clickRequestStorageAccessButton {
verifyCrossOriginCookiesPermissionPrompt(originSite, currentSite)
}.clickPagePermissionButton(allow = true) {

@ -166,12 +166,13 @@ class HomeScreenTest {
}.openThreeDotMenu {
}.openCustomizeHome {
clickPocketButton()
}.goBack {
}.goBackToHomeScreen {
verifyThoughtProvokingStories(false)
verifyStoriesByTopic(false)
}
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1821016")
@Test
fun openPocketStoryItemTest() {
activityTestRule.activityRule.applySettingsExceptions {
@ -257,12 +258,12 @@ class HomeScreenTest {
clickRecentBookmarksButton()
clickRecentSearchesButton()
clickPocketButton()
}.goBack {
}.goBackToHomeScreen {
verifyCustomizeHomepageButton(false)
}.openThreeDotMenu {
}.openCustomizeHome {
clickJumpBackInButton()
}.goBack {
}.goBackToHomeScreen {
verifyCustomizeHomepageButton(true)
}
}

@ -518,7 +518,7 @@ class LoginsTest {
}
@Test
fun verifySearchLoginsTest() {
fun searchLoginsByUsernameTest() {
val firstLoginPage = TestAssetHelper.getSaveLoginAsset(mockWebServer)
val secondLoginPage = "https://mozilla-mobile.github.io/testapp/v2.0/loginForm.html"
val originWebsite = "mozilla-mobile.github.io"
@ -529,17 +529,73 @@ class LoginsTest {
saveLoginFromPrompt("Save")
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(secondLoginPage.toUri()) {
fillAndSubmitLoginCredentials("mozilla", "firefox")
fillAndSubmitLoginCredentials("android", "firefox")
saveLoginFromPrompt("Save")
}.openThreeDotMenu {
}.openSettings {
}.openLoginsAndPasswordSubMenu {
}.openSavedLogins {
tapSetupLater()
clickSearchLoginButton()
searchLogin("ANDROID")
viewSavedLoginDetails(originWebsite)
verifyLoginItemUsername("android")
revealPassword()
verifyPasswordSaved("firefox")
}.goBackToSavedLogins {
clickSearchLoginButton()
searchLogin("android")
viewSavedLoginDetails(originWebsite)
verifyLoginItemUsername("android")
revealPassword()
verifyPasswordSaved("firefox")
}.goBackToSavedLogins {
clickSearchLoginButton()
searchLogin("AnDrOiD")
viewSavedLoginDetails(originWebsite)
verifyLoginItemUsername("android")
revealPassword()
verifyPasswordSaved("firefox")
}
}
@Test
fun searchLoginsByUrlTest() {
val firstLoginPage = TestAssetHelper.getSaveLoginAsset(mockWebServer)
val secondLoginPage = "https://mozilla-mobile.github.io/testapp/v2.0/loginForm.html"
val originWebsite = "mozilla-mobile.github.io"
navigationToolbar {
}.enterURLAndEnterToBrowser(firstLoginPage.url) {
clickSubmitLoginButton()
saveLoginFromPrompt("Save")
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(secondLoginPage.toUri()) {
fillAndSubmitLoginCredentials("android", "firefox")
saveLoginFromPrompt("Save")
}.openThreeDotMenu {
}.openSettings {
}.openLoginsAndPasswordSubMenu {
}.openSavedLogins {
tapSetupLater()
clickSearchLoginButton()
searchLogin("MOZILLA")
viewSavedLoginDetails(originWebsite)
verifyLoginItemUsername("android")
revealPassword()
verifyPasswordSaved("firefox")
}.goBackToSavedLogins {
clickSearchLoginButton()
searchLogin("mozilla")
viewSavedLoginDetails(originWebsite)
verifyLoginItemUsername("mozilla")
verifyLoginItemUsername("android")
revealPassword()
verifyPasswordSaved("firefox")
}.goBackToSavedLogins {
clickSearchLoginButton()
searchLogin("MoZiLlA")
viewSavedLoginDetails(originWebsite)
verifyLoginItemUsername("android")
revealPassword()
verifyPasswordSaved("firefox")
}

@ -13,6 +13,7 @@ import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
@ -61,6 +62,7 @@ class MediaNotificationTest {
mockWebServer.shutdown()
}
@SmokeTest
@Test
fun videoPlaybackSystemNotificationTest() {
val videoTestPage = TestAssetHelper.getVideoPageAsset(mockWebServer)
@ -94,6 +96,40 @@ class MediaNotificationTest {
mDevice.pressBack()
}
@SmokeTest
@Test
fun audioPlaybackSystemNotificationTest() {
val audioTestPage = TestAssetHelper.getAudioPageAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(audioTestPage.url) {
mDevice.waitForIdle()
clickMediaPlayerPlayButton()
assertPlaybackState(browserStore, MediaSession.PlaybackState.PLAYING)
}.openNotificationShade {
verifySystemNotificationExists(audioTestPage.title)
clickMediaNotificationControlButton("Pause")
verifyMediaSystemNotificationButtonState("Play")
}
mDevice.pressBack()
browserScreen {
assertPlaybackState(browserStore, MediaSession.PlaybackState.PAUSED)
}.openTabDrawer {
closeTab()
}
mDevice.openNotification()
notificationShade {
verifySystemNotificationGone(audioTestPage.title)
}
// close notification shade before the next test
mDevice.pressBack()
}
@Test
fun mediaSystemNotificationInPrivateModeTest() {
val audioTestPage = TestAssetHelper.getAudioPageAsset(mockWebServer)

@ -0,0 +1,121 @@
/* 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.Intent
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import mozilla.components.service.nimbus.messaging.FxNimbusMessaging
import mozilla.components.service.nimbus.messaging.MessageData
import mozilla.components.service.nimbus.messaging.Messaging
import mozilla.components.service.nimbus.messaging.StyleData
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.experiments.nimbus.Res
import org.mozilla.fenix.FenixApplication
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.RetryTestRule
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.nimbus.HomeScreenSection
import org.mozilla.fenix.nimbus.Homescreen
import org.mozilla.fenix.ui.robots.homeScreen
/**
* Tests for verifying basic functionality of the Nimbus Home Screen message
*
* Verifies a message can be displayed with all of the correct components
**/
class NimbusMessagingHomescreenTest {
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer
private var messageButtonLabel = "CLICK ME"
private var messageText = "Some Nimbus Messaging text"
private var messageTitle = "A Nimbus title"
@get:Rule
val homeActivityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides(
skipOnboarding = true,
).withIntent(
Intent().apply {
action = Intent.ACTION_VIEW
},
)
@Rule
@JvmField
val retryTestRule = RetryTestRule(3)
@Before
fun setUp() {
// Set up nimbus message
FxNimbusMessaging.features.messaging.withInitializer {
// FML generated objects.
Messaging(
messages = mapOf(
"test-message" to MessageData(
action = Res.string("TEST ACTION"),
style = "TEST STYLE",
buttonLabel = Res.string(messageButtonLabel),
text = Res.string(messageText),
title = Res.string(messageTitle),
trigger = listOf("ALWAYS"),
),
),
styles = mapOf(
"TEST STYLE" to StyleData(),
),
actions = mapOf(
"TEST ACTION" to "https://example.com",
),
triggers = mapOf(
"ALWAYS" to "true",
),
)
}
// Remove some homescreen features not needed for testing
FxNimbus.features.homescreen.withInitializer {
// These are FML generated objects and enums
Homescreen(
sectionsEnabled = mapOf(
HomeScreenSection.JUMP_BACK_IN to false,
HomeScreenSection.POCKET to false,
HomeScreenSection.POCKET_SPONSORED_STORIES to false,
HomeScreenSection.RECENT_EXPLORATIONS to false,
HomeScreenSection.RECENTLY_SAVED to false,
HomeScreenSection.TOP_SITES to false,
),
)
}
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
// refresh message store
val application = (homeActivityTestRule.activity.application as FenixApplication)
application.restoreMessaging()
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
@Test
fun testNimbusMessageIsDisplayed() {
// Checks the home screen card message is displayed correctly
homeScreen {
verifyNimbusMessageCard(messageTitle, messageText, messageButtonLabel)
}
}
}

@ -8,6 +8,8 @@ import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import kotlinx.coroutines.test.runTest
import mozilla.components.service.nimbus.messaging.FxNimbusMessaging
import mozilla.components.service.nimbus.messaging.Messaging
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
@ -16,11 +18,9 @@ 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
import org.mozilla.fenix.messaging.CustomAttributeProvider
/**
* This test is to test the integrity of messages hardcoded in the FML.
@ -45,7 +45,7 @@ class NimbusMessagingMessageTest {
fun setUp() {
context = TestHelper.appContext
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
feature = FxNimbus.features.messaging.value()
feature = FxNimbusMessaging.features.messaging.value()
}
/**

@ -0,0 +1,94 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.ui
import android.content.Context
import android.os.Build
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import androidx.test.rule.GrantPermissionRule.grant
import androidx.test.uiautomator.UiDevice
import mozilla.components.service.nimbus.messaging.FxNimbusMessaging
import org.json.JSONObject
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.experiments.nimbus.HardcodedNimbusFeatures
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.ui.robots.notificationShade
/**
* A UI test for testing the notification surface for Nimbus Messaging.
*/
class NimbusMessagingNotificationTest {
private lateinit var mDevice: UiDevice
private lateinit var context: Context
private lateinit var hardcodedNimbus: HardcodedNimbusFeatures
@get:Rule
val activityTestRule =
HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
@get:Rule
val grantPermissionRule: GrantPermissionRule =
if (Build.VERSION.SDK_INT >= 33) {
grant("android.permission.POST_NOTIFICATIONS")
} else {
grant()
}
@Before
fun setUp() {
context = TestHelper.appContext
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
}
@Test
fun testShowingNotificationMessage() {
hardcodedNimbus = HardcodedNimbusFeatures(
context,
"messaging" to JSONObject(
"""
{
"message-under-experiment": "test-default-browser-notification",
"messages": {
"test-default-browser-notification": {
"title": "preferences_set_as_default_browser",
"text": "default_browser_experiment_card_text",
"surface": "notification",
"style": "NOTIFICATION",
"action": "MAKE_DEFAULT_BROWSER",
"trigger": [
"ALWAYS"
]
}
}
}
""".trimIndent(),
),
)
// The scheduling of the Messaging Notification Worker happens in the HomeActivity
// onResume().
// We need to have connected FxNimbus to hardcodedNimbus by the time it is scheduled, so
// we finishActivity, connect, _then_ re-launch the activity so that the worker has
// hardcodedNimbus by the time its re-scheduled.
// Because the scheduling happens for a second time, the work request needs to replace the
// existing one.
activityTestRule.finishActivity()
hardcodedNimbus.connectWith(FxNimbus)
activityTestRule.launchActivity(null)
mDevice.openNotification()
notificationShade {
val data =
FxNimbusMessaging.features.messaging.value().messages["test-default-browser-notification"]
verifySystemNotificationExists(data!!.title!!)
verifySystemNotificationExists(data.text)
}
}
}

@ -6,6 +6,8 @@ package org.mozilla.fenix.ui
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import mozilla.components.service.nimbus.messaging.FxNimbusMessaging
import mozilla.components.service.nimbus.messaging.Messaging
import org.junit.Assert
import org.junit.Before
import org.junit.Rule
@ -13,11 +15,9 @@ import org.junit.Test
import org.mozilla.experiments.nimbus.NimbusInterface
import org.mozilla.experiments.nimbus.internal.NimbusException
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
import org.mozilla.fenix.messaging.CustomAttributeProvider
/**
* Test to instantiate Nimbus and automatically test all trigger expressions shipping with the app.
@ -39,7 +39,7 @@ class NimbusMessagingTriggerTest {
fun setUp() {
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
nimbus = TestHelper.appContext.components.analytics.experiments
feature = FxNimbus.features.messaging.value()
feature = FxNimbusMessaging.features.messaging.value()
}
@Test

@ -79,7 +79,7 @@ class PwaTest {
clickAddAutomaticallyButton()
}.openHomeScreenShortcut(shortcutTitle) {
clickLinkMatchingText("Telephone link")
clickOpenInAppPromptButton()
confirmOpenLinkInAnotherApp()
assertNativeAppOpens(PHONE_APP, phoneLink)
}
}

@ -181,12 +181,12 @@ class SearchTest {
}.submitQuery(queryString) {
longClickLink("Link 1")
clickContextOpenLinkInNewTab()
snackBarButtonClick()
clickSnackbarButton("SWITCH")
waitForPageToLoad()
pressBack()
longClickLink("Link 2")
clickContextOpenLinkInNewTab()
snackBarButtonClick()
clickSnackbarButton("SWITCH")
waitForPageToLoad()
}.openTabDrawer {
}.openTabsListThreeDotMenu {
@ -219,22 +219,22 @@ class SearchTest {
}.submitQuery(queryString) {
longClickLink("Link 1")
clickContextOpenLinkInNewTab()
snackBarButtonClick()
clickSnackbarButton("SWITCH")
waitForPageToLoad()
pressBack()
longClickLink("Link 1")
clickContextOpenLinkInNewTab()
snackBarButtonClick()
clickSnackbarButton("SWITCH")
waitForPageToLoad()
pressBack()
longClickLink("Link 2")
clickContextOpenLinkInNewTab()
snackBarButtonClick()
clickSnackbarButton("SWITCH")
waitForPageToLoad()
pressBack()
longClickLink("Link 1")
clickContextOpenLinkInNewTab()
snackBarButtonClick()
clickSnackbarButton("SWITCH")
waitForPageToLoad()
}.openTabDrawer {
}.openTabsListThreeDotMenu {
@ -339,12 +339,12 @@ class SearchTest {
}.submitQuery(queryString) {
longClickLink("Link 1")
clickContextOpenLinkInNewTab()
snackBarButtonClick()
clickSnackbarButton("SWITCH")
waitForPageToLoad()
mDevice.pressBack()
longClickLink("Link 2")
clickContextOpenLinkInNewTab()
snackBarButtonClick()
clickSnackbarButton("SWITCH")
waitForPageToLoad()
}.openTabDrawer {
}.openTabsListThreeDotMenu {
@ -386,12 +386,12 @@ class SearchTest {
}.submitQuery(queryString) {
longClickLink("Link 1")
clickContextOpenLinkInNewTab()
snackBarButtonClick()
clickSnackbarButton("SWITCH")
waitForPageToLoad()
mDevice.pressBack()
longClickLink("Link 2")
clickContextOpenLinkInNewTab()
snackBarButtonClick()
clickSnackbarButton("SWITCH")
waitForPageToLoad()
}.openTabDrawer {
}.openTabsListThreeDotMenu {
@ -434,12 +434,12 @@ class SearchTest {
}.submitQuery(queryString) {
longClickLink("Link 1")
clickContextOpenLinkInNewTab()
snackBarButtonClick()
clickSnackbarButton("SWITCH")
waitForPageToLoad()
mDevice.pressBack()
longClickLink("Link 2")
clickContextOpenLinkInNewTab()
snackBarButtonClick()
clickSnackbarButton("SWITCH")
waitForPageToLoad()
}.openTabDrawer {
}.openTabsListThreeDotMenu {
@ -487,12 +487,12 @@ class SearchTest {
}.submitQuery(queryString) {
longClickLink("Link 1")
clickContextOpenLinkInNewTab()
snackBarButtonClick()
clickSnackbarButton("SWITCH")
waitForPageToLoad()
mDevice.pressBack()
longClickLink("Link 2")
clickContextOpenLinkInNewTab()
snackBarButtonClick()
clickSnackbarButton("SWITCH")
waitForPageToLoad()
}.openTabDrawer {
}.openTabsListThreeDotMenu {

@ -6,17 +6,20 @@ package org.mozilla.fenix.ui
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import mozilla.components.concept.engine.utils.EngineReleaseChannel
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.ui.robots.clickAlwaysButton
import org.mozilla.fenix.helpers.TestHelper.assertYoutubeAppOpens
import org.mozilla.fenix.helpers.TestHelper.exitMenu
import org.mozilla.fenix.helpers.TestHelper.runWithCondition
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
@ -32,10 +35,7 @@ class SettingsAdvancedTest {
private lateinit var mockWebServer: MockWebServer
@get:Rule
val activityIntentTestRule = HomeActivityIntentTestRule(
isPocketEnabled = false,
isTCPCFREnabled = false,
)
val activityIntentTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
@Before
fun setUp() {
@ -53,43 +53,311 @@ class SettingsAdvancedTest {
// Walks through settings menu and sub-menus to ensure all items are present
@Test
fun settingsAboutItemsTest() {
fun settingsAdvancedItemsTest() {
// ADVANCED
homeScreen {
}.openThreeDotMenu {
}.openSettings {
// ADVANCED
verifySettingsToolbar()
verifyAdvancedHeading()
verifyAddons()
verifyOpenLinksInAppsButton()
verifyOpenLinksInAppsState("Never")
verifyRemoteDebug()
verifySettingsOptionSummary("Open links in apps", "Never")
verifyExternalDownloadManagerButton()
verifyExternalDownloadManagerToggle(false)
verifyLeakCanaryButton()
verifyLeakCanaryToggle(true)
verifyRemoteDebuggingButton()
verifyRemoteDebuggingToggle(false)
}
}
// Assumes Play Store is installed and enabled
@SmokeTest
@Test
fun openLinkInAppTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 3)
fun verifyOpenLinkInAppViewTest() {
runWithCondition(
// Returns the GeckoView channel set for the current version, if a feature is limited to Nightly or Beta.
// Once this feature lands in RC we should remove the wrapper.
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.NIGHTLY ||
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.BETA,
) {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifySettingsOptionSummary("Open links in apps", "Never")
}.openOpenLinksInAppsMenu {
verifyOpenLinksInAppsView("Never")
}
}
}
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifyOpenLinksInAppsState("Never")
}.openOpenLinksInAppsMenu {
clickAlwaysButton()
}.goBack {
}.goBack {}
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
mDevice.waitForIdle()
clickLinkMatchingText("Mozilla Playstore link")
mDevice.waitForIdle()
TestHelper.assertPlayStoreOpens()
@SmokeTest
@Test
fun verifyOpenLinkInAppViewInPrivateBrowsingTest() {
runWithCondition(
// Returns the GeckoView channel set for the current version, if a feature is limited to Nightly or Beta.
// Once this feature lands in RC we should remove the wrapper.
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.NIGHTLY ||
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.BETA,
) {
homeScreen {
}.togglePrivateBrowsingMode()
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifySettingsOptionSummary("Open links in apps", "Never")
}.openOpenLinksInAppsMenu {
verifyOpenLinksInAppsView("Never")
}
}
}
// Assumes Youtube is installed and enabled
@Test
fun neverOpenLinkInAppTest() {
runWithCondition(
// Returns the GeckoView channel set for the current version, if a feature is limited to Nightly or Beta.
// Once this feature lands in RC we should remove the wrapper.
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.NIGHTLY ||
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.BETA,
) {
val defaultWebPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifySettingsOptionSummary("Open links in apps", "Never")
}.openOpenLinksInAppsMenu {
verifyOpenLinksInAppsView("Never")
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
clickLinkMatchingText("Youtube link")
waitForPageToLoad()
verifyUrl("youtube.com")
}
}
}
// Assumes Youtube is installed and enabled
@Test
fun privateBrowsingNeverOpenLinkInAppTest() {
runWithCondition(
// Returns the GeckoView channel set for the current version, if a feature is limited to Nightly or Beta.
// Once this feature lands in RC we should remove the wrapper.
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.NIGHTLY ||
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.BETA,
) {
val defaultWebPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
homeScreen {
}.togglePrivateBrowsingMode()
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifySettingsOptionSummary("Open links in apps", "Never")
}.openOpenLinksInAppsMenu {
verifyOpenLinksInAppsView("Never")
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
clickLinkMatchingText("Youtube link")
waitForPageToLoad()
verifyUrl("youtube.com")
}
}
}
// Assumes Youtube is installed and enabled
@SmokeTest
@Test
fun askBeforeOpeningLinkInAppTest() {
runWithCondition(
// Returns the GeckoView channel set for the current version, if a feature is limited to Nightly or Beta.
// Once this feature lands in RC we should remove the wrapper.
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.NIGHTLY ||
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.BETA,
) {
val defaultWebPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifySettingsOptionSummary("Open links in apps", "Never")
}.openOpenLinksInAppsMenu {
verifyOpenLinksInAppsView("Never")
clickOpenLinkInAppOption("Ask before opening")
verifySelectedOpenLinksInAppOption("Ask before opening")
}.goBack {
verifySettingsOptionSummary("Open links in apps", "Ask before opening")
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
clickLinkMatchingText("Youtube link")
verifyOpenLinkInAnotherAppPrompt()
cancelOpenLinkInAnotherApp()
waitForPageToLoad()
verifyUrl("youtube.com")
}
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
clickLinkMatchingText("Youtube link")
verifyOpenLinkInAnotherAppPrompt()
confirmOpenLinkInAnotherApp()
mDevice.waitForIdle()
assertYoutubeAppOpens()
}
}
}
// Assumes Youtube is installed and enabled
@SmokeTest
@Test
fun privateBrowsingAskBeforeOpeningLinkInAppTest() {
runWithCondition(
// Returns the GeckoView channel set for the current version, if a feature is limited to Nightly or Beta.
// Once this feature lands in RC we should remove the wrapper.
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.NIGHTLY ||
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.BETA,
) {
val defaultWebPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
homeScreen {
}.togglePrivateBrowsingMode()
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifySettingsOptionSummary("Open links in apps", "Never")
}.openOpenLinksInAppsMenu {
verifyOpenLinksInAppsView("Never")
clickOpenLinkInAppOption("Ask before opening")
verifySelectedOpenLinksInAppOption("Ask before opening")
}.goBack {
verifySettingsOptionSummary("Open links in apps", "Ask before opening")
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
clickLinkMatchingText("Youtube link")
verifyPrivateBrowsingOpenLinkInAnotherAppPrompt("youtube.com")
cancelOpenLinkInAnotherApp()
waitForPageToLoad()
verifyUrl("youtube.com")
}
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
clickLinkMatchingText("Youtube link")
verifyPrivateBrowsingOpenLinkInAnotherAppPrompt("youtube.com")
confirmOpenLinkInAnotherApp()
mDevice.waitForIdle()
assertYoutubeAppOpens()
}
}
}
// Assumes Youtube is installed and enabled
@Test
fun alwaysOpenLinkInAppTest() {
runWithCondition(
// Returns the GeckoView channel set for the current version, if a feature is limited to Nightly or Beta.
// Once this feature lands in RC we should remove the wrapper.
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.NIGHTLY ||
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.BETA,
) {
val defaultWebPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifySettingsOptionSummary("Open links in apps", "Never")
}.openOpenLinksInAppsMenu {
verifyOpenLinksInAppsView("Never")
clickOpenLinkInAppOption("Always")
verifySelectedOpenLinksInAppOption("Always")
}.goBack {
verifySettingsOptionSummary("Open links in apps", "Always")
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
clickLinkMatchingText("Youtube link")
mDevice.waitForIdle()
assertYoutubeAppOpens()
}
}
}
// Assumes Youtube is installed and enabled
@Test
fun privateBrowsingAlwaysOpenLinkInAppTest() {
runWithCondition(
// Returns the GeckoView channel set for the current version, if a feature is limited to Nightly or Beta.
// Once this feature lands in RC we should remove the wrapper.
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.NIGHTLY ||
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.BETA,
) {
val defaultWebPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
homeScreen {
}.togglePrivateBrowsingMode()
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifySettingsOptionSummary("Open links in apps", "Never")
}.openOpenLinksInAppsMenu {
verifyOpenLinksInAppsView("Never")
clickOpenLinkInAppOption("Always")
verifySelectedOpenLinksInAppOption("Always")
}.goBack {
verifySettingsOptionSummary("Open links in apps", "Always")
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
clickLinkMatchingText("Youtube link")
verifyPrivateBrowsingOpenLinkInAnotherAppPrompt("youtube.com")
cancelOpenLinkInAnotherApp()
waitForPageToLoad()
verifyUrl("youtube.com")
}
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
clickLinkMatchingText("Youtube link")
verifyPrivateBrowsingOpenLinkInAnotherAppPrompt("youtube.com")
confirmOpenLinkInAnotherApp()
mDevice.waitForIdle()
assertYoutubeAppOpens()
}
}
}
}

@ -119,6 +119,7 @@ class SettingsDeleteBrowsingDataTest {
verifyAllCheckBoxesAreChecked()
selectOnlyOpenTabsCheckBox()
clickDeleteBrowsingDataButton()
verifyDeleteBrowsingDataDialog()
confirmDeletionAndAssertSnackbar()
}
settingsScreen {
@ -140,9 +141,11 @@ class SettingsDeleteBrowsingDataTest {
verifyAllCheckBoxesAreChecked()
selectOnlyOpenTabsCheckBox()
clickDeleteBrowsingDataButton()
verifyDeleteBrowsingDataDialog()
clickDialogCancelButton()
verifyOpenTabsCheckBox(true)
clickDeleteBrowsingDataButton()
verifyDeleteBrowsingDataDialog()
confirmDeletionAndAssertSnackbar()
}
settingsScreen {
@ -174,9 +177,11 @@ class SettingsDeleteBrowsingDataTest {
verifyBrowsingHistoryDetails("2")
selectOnlyBrowsingHistoryCheckBox()
clickDeleteBrowsingDataButton()
verifyDeleteBrowsingDataDialog()
clickDialogCancelButton()
verifyBrowsingHistoryDetails(true)
clickDeleteBrowsingDataButton()
verifyDeleteBrowsingDataDialog()
confirmDeletionAndAssertSnackbar()
verifyBrowsingHistoryDetails("0")
exitMenu()
@ -213,6 +218,7 @@ class SettingsDeleteBrowsingDataTest {
}.openSettingsSubMenuDeleteBrowsingData {
selectOnlyCookiesCheckBox()
clickDeleteBrowsingDataButton()
verifyDeleteBrowsingDataDialog()
confirmDeletionAndAssertSnackbar()
exitMenu()
}
@ -242,6 +248,7 @@ class SettingsDeleteBrowsingDataTest {
}.openSettingsSubMenuDeleteBrowsingData {
selectOnlyCachedFilesCheckBox()
clickDeleteBrowsingDataButton()
verifyDeleteBrowsingDataDialog()
confirmDeletionAndAssertSnackbar()
exitMenu()
}

@ -50,7 +50,7 @@ class SettingsDeveloperToolsTest {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyRemoteDebug()
verifyRemoteDebuggingButton()
}
}

@ -34,7 +34,7 @@ import java.util.Locale
* Tests for verifying the General section of the Settings menu
*
*/
class SettingsBasicsTest {
class SettingsGeneralTest {
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
private lateinit var mockWebServer: MockWebServer
@ -62,14 +62,18 @@ class SettingsBasicsTest {
verifySettingsToolbar()
verifyGeneralHeading()
verifySearchButton()
verifySettingsOptionSummary("Search", "Google")
verifyTabsButton()
verifySettingsOptionSummary("Tabs", "Close manually")
verifyHomepageButton()
verifySettingsOptionSummary("Homepage", "Open on homepage after four hours")
verifyCustomizeButton()
verifyLoginsAndPasswordsButton()
verifyAutofillButton()
verifyAccessibilityButton()
verifyLanguageButton()
verifySetAsDefaultBrowserButton()
verifyDefaultBrowserToggle(false)
}
}
@ -167,4 +171,51 @@ class SettingsBasicsTest {
}
}
}
@SmokeTest
@Test
fun verifyHomepageOptionSummaryUpdatesTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifySettingsOptionSummary("Homepage", "Open on homepage after four hours")
}.openHomepageSubMenu {
verifySelectedOpeningScreenOption("Homepage after four hours of inactivity")
clickOpeningScreenOption("Homepage")
verifySelectedOpeningScreenOption("Homepage")
}.goBack {
verifySettingsOptionSummary("Homepage", "Open on homepage")
}.openHomepageSubMenu {
clickOpeningScreenOption("Last tab")
verifySelectedOpeningScreenOption("Last tab")
}.goBack {
verifySettingsOptionSummary("Homepage", "Open on last tab")
}
}
@Test
fun verifyTabsOptionSummaryUpdatesTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyTabsButton()
verifySettingsOptionSummary("Tabs", "Close manually")
}.openTabsSubMenu {
verifySelectedCloseTabsOption("Never")
clickClosedTabsOption("After one day")
verifySelectedCloseTabsOption("After one day")
}.goBack {
verifySettingsOptionSummary("Tabs", "Close after one day")
}.openTabsSubMenu {
clickClosedTabsOption("After one week")
verifySelectedCloseTabsOption("After one week")
}.goBack {
verifySettingsOptionSummary("Tabs", "Close after one week")
}.openTabsSubMenu {
clickClosedTabsOption("After one month")
verifySelectedCloseTabsOption("After one month")
}.goBack {
verifySettingsOptionSummary("Tabs", "Close after one month")
}
}
}

@ -68,6 +68,8 @@ class SettingsHTTPSOnlyModeTest {
allTabsOptionSelected = true,
privateTabsOptionSelected = false,
)
}.goBack {
verifySettingsOptionSummary("HTTPS-Only Mode", "On in all tabs")
exitMenu()
}
navigationToolbar {
@ -127,6 +129,8 @@ class SettingsHTTPSOnlyModeTest {
allTabsOptionSelected = false,
privateTabsOptionSelected = true,
)
}.goBack {
verifySettingsOptionSummary("HTTPS-Only Mode", "On in private tabs")
exitMenu()
}
navigationToolbar {
@ -183,6 +187,8 @@ class SettingsHTTPSOnlyModeTest {
}.openHttpsOnlyModeMenu {
clickHttpsOnlyModeSwitch()
verifyHttpsOnlyModeIsEnabled(false)
}.goBack {
verifySettingsOptionSummary("HTTPS-Only Mode", "Off")
exitMenu()
}
navigationToolbar {

@ -75,7 +75,7 @@ class SettingsHomepageTest {
}.openThreeDotMenu {
}.openCustomizeHome {
clickShortcutsButton()
}.goBack {
}.goBackToHomeScreen {
defaultTopSites.forEach { item ->
verifyNotExistingTopSitesList(item)
}
@ -96,7 +96,7 @@ class SettingsHomepageTest {
}.openThreeDotMenu {
}.openCustomizeHome {
clickRecentlyVisited()
}.goBack {
}.goBackToHomeScreen {
verifyRecentlyVisitedSectionIsNotDisplayed()
}
}
@ -116,7 +116,7 @@ class SettingsHomepageTest {
}.openThreeDotMenu {
}.openCustomizeHome {
clickPocketButton()
}.goBack {
}.goBackToHomeScreen {
verifyPocketSectionIsNotDisplayed()
}
}
@ -133,7 +133,7 @@ class SettingsHomepageTest {
}.openThreeDotMenu {
}.openCustomizeHome {
clickJumpBackInButton()
}.goBack {
}.goBackToHomeScreen {
verifyJumpBackInSectionIsNotDisplayed()
}
}
@ -152,7 +152,7 @@ class SettingsHomepageTest {
}.openThreeDotMenu {
}.openCustomizeHome {
clickRecentBookmarksButton()
}.goBack {
}.goBackToHomeScreen {
verifyRecentBookmarksSectionIsNotDisplayed()
}
}
@ -167,7 +167,7 @@ class SettingsHomepageTest {
}.openThreeDotMenu {
}.openSettings {
}.openHomepageSubMenu {
clickStartOnHomepageButton()
clickOpeningScreenOption("Homepage")
}
restartApp(activityIntentTestRule)
@ -187,7 +187,7 @@ class SettingsHomepageTest {
}.goToHomescreen {
}.openThreeDotMenu {
}.openCustomizeHome {
clickStartOnLastTabButton()
clickOpeningScreenOption("Last tab")
}
restartApp(activityIntentTestRule)
@ -205,8 +205,8 @@ class SettingsHomepageTest {
}.openThreeDotMenu {
}.openSettings {
}.openHomepageSubMenu {
clickStartOnHomepageButton()
}.goBack {}
clickOpeningScreenOption("Homepage")
}.goBackToHomeScreen {}
with(activityIntentTestRule) {
finishActivity()

@ -0,0 +1,158 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.ui
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.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 the privacy and security section of the Settings menu
*
*/
class SettingsPrivacyTest {
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer
@get:Rule
val activityTestRule = HomeActivityTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
@Before
fun setUp() {
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
@Test
fun settingsPrivacyItemsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifySettingsToolbar()
verifyPrivacyHeading()
verifyPrivateBrowsingButton()
verifyHTTPSOnlyModeButton()
verifySettingsOptionSummary("HTTPS-Only Mode", "Off")
verifyCookieBannerReductionButton()
verifySettingsOptionSummary("Cookie Banner Reduction", "Off")
verifyEnhancedTrackingProtectionButton()
verifySettingsOptionSummary("Enhanced Tracking Protection", "Standard")
verifySitePermissionsButton()
verifyDeleteBrowsingDataButton()
verifyDeleteBrowsingDataOnQuitButton()
verifySettingsOptionSummary("Delete browsing data on quit", "Off")
verifyNotificationsButton()
verifySettingsOptionSummary("Notifications", "Allowed")
verifyDataCollectionButton()
}
}
@Test
fun verifyDataCollectionTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDataCollection {
verifyDataCollectionView(
true,
true,
"On",
)
}
}
@Test
fun verifyUsageAndTechnicalDataToggleTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDataCollection {
verifyUsageAndTechnicalDataToggle(true)
clickUsageAndTechnicalDataToggle()
verifyUsageAndTechnicalDataToggle(false)
}
}
@Test
fun verifyMarketingDataToggleTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDataCollection {
verifyMarketingDataToggle(true)
clickMarketingDataToggle()
verifyMarketingDataToggle(false)
}
}
@Test
fun verifyStudiesToggleTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDataCollection {
verifyDataCollectionView(
true,
true,
"On",
)
clickStudiesOption()
verifyStudiesToggle(true)
clickStudiesToggle()
verifyStudiesDialog()
clickStudiesDialogCancelButton()
verifyStudiesToggle(true)
}
}
@Test
fun sitePermissionsItemsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuSitePermissions {
verifySitePermissionsToolbarTitle()
verifyToolbarGoBackButton()
verifySitePermissionOption("Autoplay", "Block audio only")
verifySitePermissionOption("Camera", "Blocked by Android")
verifySitePermissionOption("Location", "Blocked by Android")
verifySitePermissionOption("Microphone", "Blocked by Android")
verifySitePermissionOption("Notification", "Ask to allow")
verifySitePermissionOption("Persistent Storage", "Ask to allow")
verifySitePermissionOption("Cross-site cookies", "Ask to allow")
verifySitePermissionOption("DRM-controlled content", "Ask to allow")
verifySitePermissionOption("Exceptions")
}
}
@Test
fun notificationPermissionsItemsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuSitePermissions {
}.openNotification {
verifyNotificationSubMenuItems()
}
}
}

@ -98,6 +98,7 @@ class SettingsSearchTest {
}
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1807268")
@Test
fun toggleSearchBookmarksAndHistoryTest() {
val page1 = getGenericAsset(mockWebServer, 1)
@ -201,6 +202,13 @@ class SettingsSearchTest {
changeDefaultSearchEngine(activityTestRule, searchEngine)
}.submitQuery("mozilla ") {
verifyUrl(searchEngine)
}.openThreeDotMenu {
}.openSettings {
verifySettingsOptionSummary("Search", "Google")
}.openSearchSubMenu {
changeDefaultSearchEngine(searchEngine)
}.goBack {
verifySettingsOptionSummary("Search", searchEngine)
}
}
@ -232,6 +240,7 @@ class SettingsSearchTest {
saveEditSearchEngine()
changeDefaultSearchEngine(searchEngine.newTitle)
}.goBack {
verifySettingsOptionSummary("Search", searchEngine.newTitle)
}.goBack {
}.openSearch {
verifyDefaultSearchEngine(searchEngine.newTitle)

@ -1,236 +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 androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.ui.robots.homeScreen
/**
* Tests for verifying the main three dot menu options
*
*/
class SettingsTest {
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer
@get:Rule
val activityTestRule = HomeActivityTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
@Before
fun setUp() {
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@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()
}
}
// Walks through settings menu and sub-menus to ensure all items are present
@Ignore("This is a stub test, ignore for now")
@Test
fun settingsMenusItemsTest() {
// SYNC
// see: SettingsSyncTest
// BASICS
// see: SettingsBasicsTest
// PRIVACY
// see: SettingsPrivacyTest
// DEVELOPER TOOLS
// Verify header: "Developer Tools"
// Verify item: "Remote debugging via USB" and default toggle value: "Off"
// ABOUT
// Verify header: "About"
// Verify item: "Help"
// Verify item: "Rate on Google Play"
// Verify item: "About Firefox Preview"
//
}
// SYNC
// see: SettingsSyncTest
// BASICS
// see: SettingsBasicsTest
//
// PRIVACY
// see: SettingsPrivacyTest
// DEVELOPER TOOLS
@Ignore("This is a stub test, ignore for now")
@Test
fun turnOnRemoteDebuggingViaUsb() {
// Open terminal
// Verify USB debugging is off
// Open 3dot (main) menu
// Select settings
// Toggle Remote debugging via USB to 'on'
// Open terminal
// Verify USB debugging is on
}
// ABOUT
@Ignore("This is a stub test, ignore for now")
@Test
fun verifyHelpRedirect() {
// Open 3dot (main) menu
// Select settings
// Click on "Help"
// Verify redirect to: https://support.mozilla.org/
}
@Ignore("This is a stub test, ignore for now")
@Test
fun verifyRateOnGooglePlayRedirect() {
// Open 3dot (main) menu
// Select settings
// Click on "Rate on Google Play"
// Verify Android "Open with Google Play Store" sub menu
}
@Ignore("This is a stub test, ignore for now")
@Test
fun verifyAboutFirefoxPreview() {
// Open 3dot (main) menu
// Select settings
// Click on "Verify About Firefox Preview"
// Verify about page contains....
// Build #
// Version #
// "Firefox Preview is produced by Mozilla"
// Day, Date, timestamp
// "Open source libraries we use"
}
}

@ -24,12 +24,11 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.Constants.PackageName.YOUTUBE_APP
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.assertNativeAppOpens
import org.mozilla.fenix.helpers.TestHelper.assertYoutubeAppOpens
import org.mozilla.fenix.helpers.TestHelper.createCustomTabIntent
import org.mozilla.fenix.helpers.TestHelper.registerAndCleanupIdlingResources
import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource
@ -37,9 +36,6 @@ import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.customTabScreen
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.notificationShade
import org.mozilla.fenix.ui.robots.openEditURLView
import org.mozilla.fenix.ui.robots.searchScreen
/**
* Test Suite that contains a part of the Smoke and Sanity tests defined in TestRail:
@ -255,7 +251,7 @@ class SmokeTest {
verifyNotificationDotOnMainMenu()
}.openThreeDotMenu {
}.clickOpenInApp {
assertNativeAppOpens(YOUTUBE_APP, youtubeURL)
assertYoutubeAppOpens()
}
}
@ -610,39 +606,6 @@ class SmokeTest {
}
}
@Test
fun audioPlaybackSystemNotificationTest() {
val audioTestPage = TestAssetHelper.getAudioPageAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(audioTestPage.url) {
mDevice.waitForIdle()
clickMediaPlayerPlayButton()
assertPlaybackState(browserStore, MediaSession.PlaybackState.PLAYING)
}.openNotificationShade {
verifySystemNotificationExists(audioTestPage.title)
clickMediaNotificationControlButton("Pause")
verifyMediaSystemNotificationButtonState("Play")
}
mDevice.pressBack()
browserScreen {
assertPlaybackState(browserStore, MediaSession.PlaybackState.PAUSED)
}.openTabDrawer {
closeTab()
}
mDevice.openNotification()
notificationShade {
verifySystemNotificationGone(audioTestPage.title)
}
// close notification shade before the next test
mDevice.pressBack()
}
@Test
fun tabMediaControlButtonTest() {
val audioTestPage = TestAssetHelper.getAudioPageAsset(mockWebServer)
@ -668,7 +631,7 @@ class SmokeTest {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyDefaultBrowserIsDisabled()
verifyDefaultBrowserToggle(false)
clickDefaultBrowserSwitch()
verifyAndroidDefaultAppsMenuAppears()
}
@ -676,45 +639,6 @@ class SmokeTest {
mDevice.pressBack()
}
@Test
fun copyTextTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
longClickAndCopyText("content")
}.openNavigationToolbar {
openEditURLView()
}
searchScreen {
clickClearButton()
longClickToolbar()
clickPasteText()
verifyTypedToolbarText("content")
}
}
@Test
fun selectAllAndCopyTextTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
longClickAndCopyText("content", true)
}.openNavigationToolbar {
openEditURLView()
}
searchScreen {
clickClearButton()
longClickToolbar()
clickPasteText()
// with Select all, some white space is copied over, so we need to include that too
verifyTypedToolbarText(" Page content: 1 ")
}
}
@Test
fun goToHomeScreenBottomToolbarTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)

@ -63,7 +63,7 @@ class SponsoredShortcutsTest {
verifySponsoredShortcutsCheckBox(true)
clickSponsoredShortcuts()
verifySponsoredShortcutsCheckBox(false)
}.goBack {
}.goBackToHomeScreen {
verifyNotExistingSponsoredTopSitesList()
}
}
@ -98,6 +98,7 @@ class SponsoredShortcutsTest {
}
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1807268")
@Test
fun verifySponsoredShortcutsSettingsOptionTest() {
homeScreen {

@ -0,0 +1,225 @@
package org.mozilla.fenix.ui
import androidx.core.net.toUri
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.RetryTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.openEditURLView
import org.mozilla.fenix.ui.robots.searchScreen
class TextSelectionTest {
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"
@get:Rule
val activityIntentTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
@Rule
@JvmField
val retryTestRule = RetryTestRule(3)
@Before
fun setUp() {
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
@SmokeTest
@Test
fun selectAllAndCopyTextTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
longClickAndCopyText("content", true)
}.openNavigationToolbar {
openEditURLView()
}
searchScreen {
clickClearButton()
longClickToolbar()
clickPasteText()
// With Select all, white spaces are copied
// Potential bug https://bugzilla.mozilla.org/show_bug.cgi?id=1821310
verifyTypedToolbarText(" Page content: 1 ")
}
}
@SmokeTest
@Test
fun copyTextTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
longClickAndCopyText("content")
}.openNavigationToolbar {
openEditURLView()
}
searchScreen {
clickClearButton()
longClickToolbar()
clickPasteText()
verifyTypedToolbarText("content")
}
}
@SmokeTest
@Test
fun shareSelectedTextTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
longClickLink(genericURL.content)
}.clickShareSelectedText {
verifyAndroidShareLayout()
}
}
@SmokeTest
@Test
fun selectAndSearchTextTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
longClickAndSearchText("Search", "content")
mDevice.waitForIdle()
verifyTabCounter("2")
verifyUrl("google")
}
}
@SmokeTest
@Test
fun privateSelectAndSearchTextTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.togglePrivateBrowsingMode()
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
longClickAndSearchText("Private Search", "content")
mDevice.waitForIdle()
verifyTabCounter("2")
verifyUrl("google")
}
}
@SmokeTest
@Test
fun selectAllAndCopyPDFTextTest() {
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
clickLinkMatchingText(pdfFileName)
verifyUrl(pdfFileURL)
verifyPageContent(pdfFileContent)
longClickAndCopyText("Crossing", true)
}.openNavigationToolbar {
openEditURLView()
}
searchScreen {
clickClearButton()
longClickToolbar()
clickPasteText()
// With Select all, white spaces are copied
// Potential bug https://bugzilla.mozilla.org/show_bug.cgi?id=1821310
verifyTypedToolbarText(" Washington Crossing the Delaware ")
}
}
@SmokeTest
@Test
fun copyPDFTextTest() {
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
clickLinkMatchingText(pdfFileName)
verifyUrl(pdfFileURL)
verifyPageContent(pdfFileContent)
longClickAndCopyText("Crossing")
}.openNavigationToolbar {
openEditURLView()
}
searchScreen {
clickClearButton()
longClickToolbar()
clickPasteText()
verifyTypedToolbarText("Crossing")
}
}
@SmokeTest
@Test
fun shareSelectedPDFTextTest() {
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
clickLinkMatchingText(pdfFileName)
verifyUrl(pdfFileURL)
verifyPageContent(pdfFileContent)
longClickMatchingText("Crossing")
}.clickShareSelectedText {
verifyAndroidShareLayout()
}
}
@SmokeTest
@Test
fun selectAndSearchPDFTextTest() {
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
clickLinkMatchingText(pdfFileName)
verifyUrl(pdfFileURL)
verifyPageContent(pdfFileContent)
longClickAndSearchText("Search", "Crossing")
verifyTabCounter("3")
verifyUrl("google")
}
}
@SmokeTest
@Test
fun privateSelectAndSearchPDFTextTest() {
homeScreen {
}.togglePrivateBrowsingMode()
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
clickLinkMatchingText(pdfFileName)
verifyUrl(pdfFileURL)
verifyPageContent(pdfFileContent)
longClickAndSearchText("Private Search", "Crossing")
verifyTabCounter("3")
verifyUrl("google")
}
}
}

@ -82,7 +82,7 @@ class ThreeDotMenuMainTest {
}.openThreeDotMenu {
}.openCustomizeHome {
verifyHomePageView()
}.goBack {
}.goBackToHomeScreen {
}.openThreeDotMenu {
}.openSettings {
verifySettingsView()
@ -133,7 +133,7 @@ class ThreeDotMenuMainTest {
}.openThreeDotMenu {
}.openCustomizeHome {
verifyHomePageView()
}.goBack {
}.goBackToHomeScreen {
}.openThreeDotMenu {
}.openSettings {
verifySettingsView()
@ -161,7 +161,7 @@ class ThreeDotMenuMainTest {
}.enterURLAndEnterToBrowser(webPage.url) {
longClickLink("Link 2")
clickContextOpenLinkInNewTab()
snackBarButtonClick()
clickSnackbarButton("SWITCH")
}.openThreeDotMenu {
verifyDesktopSiteModeEnabled(false)
}
@ -191,7 +191,7 @@ class ThreeDotMenuMainTest {
}.enterURLAndEnterToBrowser(webPage.url) {
longClickLink("Link 2")
clickContextOpenLinkInPrivateTab()
snackBarButtonClick()
clickSnackbarButton("SWITCH")
}.openThreeDotMenu {
verifyDesktopSiteModeEnabled(false)
}

@ -170,7 +170,7 @@ class WebControlsTest {
navigationToolbar {
}.enterURLAndEnterToBrowser(externalLinksPage.url) {
clickLinkMatchingText("Email link")
clickOpenInAppPromptButton()
confirmOpenLinkInAnotherApp()
assertNativeAppOpens(Constants.PackageName.GMAIL_APP, emailLink)
}
}
@ -182,7 +182,7 @@ class WebControlsTest {
navigationToolbar {
}.enterURLAndEnterToBrowser(externalLinksPage.url) {
clickLinkMatchingText("Telephone link")
clickOpenInAppPromptButton()
confirmOpenLinkInAnotherApp()
assertNativeAppOpens(Constants.PackageName.PHONE_APP, phoneLink)
}
}

@ -32,8 +32,17 @@ import org.hamcrest.Matchers.containsString
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
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.assertItemWithResIdExists
import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
import org.mozilla.fenix.helpers.TestHelper.getStringResource
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click
@ -90,13 +99,17 @@ class BookmarksRobot {
fun verifyCopySnackBarText() = assertSnackBarText("URL copied")
fun verifyEditBookmarksView() = assertEditBookmarksView()
fun verifyBookmarkNameEditBox() = assertBookmarkNameEditBox()
fun verifyBookmarkURLEditBox() = assertBookmarkURLEditBox()
fun verifyParentFolderSelector() = assertBookmarkFolderSelector()
fun verifyEditBookmarksView() {
assertItemWithDescriptionExists(itemWithDescription("Navigate up"))
assertItemContainingTextExists(itemWithText(getStringResource(R.string.edit_bookmark_fragment_title)))
assertItemWithResIdExists(
itemWithResId("$packageName:id/delete_bookmark_button"),
itemWithResId("$packageName:id/save_bookmark_button"),
itemWithResId("$packageName:id/bookmarkNameEdit"),
itemWithResId("$packageName:id/bookmarkUrlEdit"),
itemWithResId("$packageName:id/bookmarkParentFolderSelector"),
)
}
fun verifyKeyboardHidden() = assertKeyboardVisibility(isExpectedToBeVisible = false)
@ -265,6 +278,24 @@ class BookmarksRobot {
HomeScreenRobot().interact()
return HomeScreenRobot.Transition()
}
fun closeEditBookmarkSection(interact: BookmarksRobot.() -> Unit): BookmarksRobot.Transition {
goBackButton().click()
BookmarksRobot().interact()
return BookmarksRobot.Transition()
}
fun openBookmarkWithTitle(bookmarkTitle: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
itemWithResIdAndText("$packageName:id/title", bookmarkTitle)
.also {
it.waitForExists(waitingTime)
it.clickAndWaitForNewWindow(waitingTimeShort)
}
BrowserRobot().interact()
return BrowserRobot.Transition()
}
}
}
@ -395,21 +426,6 @@ private fun assertUndoDeleteSnackBarButton() =
private fun assertSnackBarText(text: String) =
snackBarText().check(matches(withText(containsString(text))))
private fun assertEditBookmarksView() = onView(withText("Edit bookmark"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun assertBookmarkNameEditBox() =
onView(withId(R.id.bookmarkNameEdit))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun assertBookmarkFolderSelector() =
onView(withId(R.id.bookmarkParentFolderSelector))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun assertBookmarkURLEditBox() =
onView(withId(R.id.bookmarkUrlEdit))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun assertKeyboardVisibility(isExpectedToBeVisible: Boolean) =
assertEquals(
isExpectedToBeVisible,

@ -45,8 +45,12 @@ 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.assertItemContainingTextExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithDescriptionExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdAndTextExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdExists
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
import org.mozilla.fenix.helpers.SessionLoadedIdlingResource
@ -89,7 +93,7 @@ class BrowserRobot {
}
fun verifyWhatsNewURL() {
verifyUrl("support.mozilla.org/")
verifyUrl("mozilla.org/")
}
fun verifyRateOnGooglePlayURL() {
@ -395,17 +399,18 @@ class BrowserRobot {
searchText.click()
}
fun snackBarButtonClick() {
val switchButton =
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/snackbar_btn"),
)
switchButton.waitForExists(waitingTime)
switchButton.clickAndWaitForNewWindow(waitingTime)
}
fun clickSnackbarButton(expectedText: String) =
itemWithResIdAndText("$packageName:id/snackbar_btn", expectedText)
.also {
it.waitForExists(waitingTime)
it.click()
}
fun clickSubmitLoginButton() = clickPageObject(webPageItemWithResourceId("submit"))
fun clickSubmitLoginButton() {
clickPageObject(webPageItemWithResourceId("submit"))
webPageItemWithResourceId("submit").waitUntilGone(waitingTime)
mDevice.waitForIdle(waitingTimeLong)
}
fun verifyUpdateLoginPromptIsShown() = mDevice.waitNotNull(Until.findObjects(text("Update")))
@ -539,8 +544,23 @@ class BrowserRobot {
}
fun clickSelectAddressButton() {
selectAddressButton.waitForExists(waitingTime)
selectAddressButton.clickAndWaitForNewWindow(waitingTime)
for (i in 1..RETRY_COUNT) {
try {
assertTrue(selectAddressButton.waitForExists(waitingTime))
selectAddressButton.clickAndWaitForNewWindow(waitingTime)
break
} catch (e: AssertionError) {
// Retrying, in case we hit https://bugzilla.mozilla.org/show_bug.cgi?id=1816869
// This should be removed when the bug is fixed.
if (i == RETRY_COUNT) {
throw e
} else {
clickPageObject(webPageItemWithResourceId("city"))
clickPageObject(webPageItemWithResourceId("country"))
}
}
}
}
fun verifySelectAddressButtonExists(exists: Boolean) = assertItemWithResIdExists(selectAddressButton, exists = exists)
@ -744,15 +764,34 @@ class BrowserRobot {
fun clickSetCookiesButton() = clickPageObject(webPageItemWithResourceId("setCookies"))
fun verifyCookiesProtectionHint() {
val hintMessage =
mDevice.findObject(
UiSelector()
.textContains(getStringResource(R.string.tcp_cfr_message)),
fun verifyCookiesProtectionHintIsDisplayed(isDisplayed: Boolean) {
if (isDisplayed) {
assertItemContainingTextExists(
totalCookieProtectionHintMessage,
totalCookieProtectionHintLearnMoreLink,
)
assertItemWithDescriptionExists(
totalCookieProtectionHintCloseButton,
)
assertTrue(hintMessage.waitForExists(waitingTime))
} else {
assertItemContainingTextExists(
totalCookieProtectionHintMessage,
totalCookieProtectionHintLearnMoreLink,
exists = isDisplayed,
)
assertItemWithDescriptionExists(
totalCookieProtectionHintCloseButton,
exists = isDisplayed,
)
}
}
fun clickTotalCookieProtectionLearnMoreLink() =
totalCookieProtectionHintLearnMoreLink.clickAndWaitForNewWindow(waitingTime)
fun clickTotalCookieProtectionCloseButton() =
totalCookieProtectionHintCloseButton.click()
fun clickForm(formType: String) {
when (formType) {
"Calendar Form" -> {
@ -1001,13 +1040,40 @@ class BrowserRobot {
it.click()
}
fun clickOpenInAppPromptButton() =
fun verifyOpenLinkInAnotherAppPrompt() {
assertItemWithResIdExists(itemWithResId("$packageName:id/parentPanel"))
assertItemContainingTextExists(
itemContainingText(
getStringResource(R.string.mozac_feature_applinks_normal_confirm_dialog_title),
),
itemContainingText(
getStringResource(R.string.mozac_feature_applinks_normal_confirm_dialog_message),
),
)
}
fun verifyPrivateBrowsingOpenLinkInAnotherAppPrompt(url: String) =
assertItemContainingTextExists(
itemContainingText(
getStringResource(R.string.mozac_feature_applinks_confirm_dialog_title),
),
itemContainingText(url),
)
fun confirmOpenLinkInAnotherApp() =
itemWithResIdAndText("android:id/button1", "OPEN")
.also {
it.waitForExists(waitingTime)
it.click()
}
fun cancelOpenLinkInAnotherApp() =
itemWithResIdAndText("android:id/button2", "CANCEL")
.also {
it.waitForExists(waitingTime)
it.click()
}
fun verifyFindInPageBar(exists: Boolean) =
assertItemWithResIdExists(
itemWithResId("$packageName:id/findInPageView"),
@ -1196,6 +1262,7 @@ class BrowserRobot {
}
fun clickRequestStorageAccessButton(interact: SitePermissionsRobot.() -> Unit): SitePermissionsRobot.Transition {
webPageItemContainingText("requestStorageAccess()").waitForExists(waitingTime)
clickPageObject(webPageItemContainingText("requestStorageAccess()"))
SitePermissionsRobot().interact()
@ -1378,3 +1445,9 @@ private val currentDay = currentDate.dayOfMonth
private val currentMonth = currentDate.month
private val currentYear = currentDate.year
private val cookieBanner = itemWithResId("CybotCookiebotDialog")
private val totalCookieProtectionHintMessage =
itemContainingText(getStringResource(R.string.tcp_cfr_message))
private val totalCookieProtectionHintLearnMoreLink =
itemContainingText(getStringResource(R.string.tcp_cfr_learn_more))
private val totalCookieProtectionHintCloseButton =
itemWithDescription(getStringResource(R.string.mozac_cfr_dismiss_button_content_description))

@ -11,12 +11,14 @@ import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.RootMatchers
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import junit.framework.TestCase.assertTrue
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.containsString
import org.hamcrest.Matchers.not
import org.mozilla.fenix.R
@ -35,23 +37,118 @@ class EnhancedTrackingProtectionRobot {
fun verifyEnhancedTrackingProtectionSheetStatus(status: String, state: Boolean) =
assertEnhancedTrackingProtectionSheetStatus(status, state)
fun verifyEnhancedTrackingProtectionDetailsStatus(status: String) =
assertEnhancedTrackingProtectionDetailsStatus(status)
fun verifyETPSwitchVisibility(visible: Boolean) = assertETPSwitchVisibility(visible)
fun verifyTrackingCookiesBlocked() = assertTrackingCookiesBlocked()
fun verifyCrossSiteCookiesBlocked(isBlocked: Boolean) {
assertTrue(
mDevice.findObject(UiSelector().resourceId("$packageName:id/cross_site_tracking"))
.waitForExists(waitingTime),
)
crossSiteCookiesBlockListButton.click()
// Verifies the trackers block/allow list
onView(withId(R.id.details_blocking_header))
.check(
matches(
withText(
if (isBlocked) {
("Blocked")
} else {
("Allowed")
},
),
),
)
}
fun verifySocialMediaTrackersBlocked(isBlocked: Boolean) {
assertTrue(
mDevice.findObject(UiSelector().resourceId("$packageName:id/social_media_trackers"))
.waitForExists(waitingTime),
)
socialTrackersBlockListButton.click()
// Verifies the trackers block/allow list
onView(withId(R.id.details_blocking_header))
.check(
matches(
withText(
if (isBlocked) {
("Blocked")
} else {
("Allowed")
},
),
),
)
onView(withId(R.id.blocking_text_list)).check(matches(isDisplayed()))
}
fun verifyFingerprintersBlocked() = assertFingerprintersBlocked()
fun verifyFingerprintersBlocked(isBlocked: Boolean) {
assertTrue(
mDevice.findObject(UiSelector().resourceId("$packageName:id/fingerprinters"))
.waitForExists(waitingTime),
)
fingerprintersBlockListButton.click()
// Verifies the trackers block/allow list
onView(withId(R.id.details_blocking_header))
.check(
matches(
withText(
if (isBlocked) {
("Blocked")
} else {
("Allowed")
},
),
),
)
onView(withId(R.id.blocking_text_list)).check(matches(isDisplayed()))
}
fun verifyCryptominersBlocked() = assertCryptominersBlocked()
fun verifyCryptominersBlocked(isBlocked: Boolean) {
assertTrue(
mDevice.findObject(UiSelector().resourceId("$packageName:id/cryptominers"))
.waitForExists(waitingTime),
)
cryptominersBlockListButton.click()
// Verifies the trackers block/allow list
onView(withId(R.id.details_blocking_header))
.check(
matches(
withText(
if (isBlocked) {
("Blocked")
} else {
("Allowed")
},
),
),
)
onView(withId(R.id.blocking_text_list)).check(matches(isDisplayed()))
}
fun verifyTrackingContentBlocked() = assertTrackingContentBlocked()
fun verifyTrackingContentBlocked(isBlocked: Boolean) {
assertTrue(
mDevice.findObject(UiSelector().text("Tracking Content"))
.waitForExists(waitingTime),
)
trackingContentBlockListButton.click()
// Verifies the trackers block/allow list
onView(withId(R.id.details_blocking_header))
.check(
matches(
withText(
if (isBlocked) {
("Blocked")
} else {
("Allowed")
},
),
),
)
onView(withId(R.id.blocking_text_list)).check(matches(isDisplayed()))
}
fun viewTrackingContentBlockList() {
trackingContentBlockListButton()
.check(matches(isDisplayed()))
.click()
onView(withId(R.id.blocking_text_list))
.check(
matches(
@ -66,10 +163,14 @@ class EnhancedTrackingProtectionRobot {
)
}
fun navigateBackToDetails() {
onView(withId(R.id.details_back)).click()
}
class Transition {
fun openEnhancedTrackingProtectionSheet(interact: EnhancedTrackingProtectionRobot.() -> Unit): Transition {
openEnhancedTrackingProtectionSheet().waitForExists(waitingTime)
openEnhancedTrackingProtectionSheet().click()
pageSecurityIndicator().waitForExists(waitingTime)
pageSecurityIndicator().click()
assertSecuritySheetIsCompletelyDisplayed()
EnhancedTrackingProtectionRobot().interact()
@ -84,20 +185,20 @@ class EnhancedTrackingProtectionRobot {
return BrowserRobot.Transition()
}
fun disableEnhancedTrackingProtectionFromSheet(interact: EnhancedTrackingProtectionRobot.() -> Unit): Transition {
fun toggleEnhancedTrackingProtectionFromSheet(interact: EnhancedTrackingProtectionRobot.() -> Unit): Transition {
enhancedTrackingProtectionSwitch().click()
EnhancedTrackingProtectionRobot().interact()
return Transition()
}
fun openProtectionSettings(interact: SettingsSubMenuEnhancedTrackingProtectionRobot.() -> Unit): Transition {
fun openProtectionSettings(interact: SettingsSubMenuEnhancedTrackingProtectionRobot.() -> Unit): SettingsSubMenuEnhancedTrackingProtectionRobot.Transition {
openEnhancedTrackingProtectionDetails().waitForExists(waitingTime)
openEnhancedTrackingProtectionDetails().click()
trackingProtectionSettingsButton().click()
SettingsSubMenuEnhancedTrackingProtectionRobot().interact()
return Transition()
return SettingsSubMenuEnhancedTrackingProtectionRobot.Transition()
}
fun openDetails(interact: EnhancedTrackingProtectionRobot.() -> Unit): Transition {
@ -126,7 +227,7 @@ private fun assertETPSwitchVisibility(visible: Boolean) {
}
private fun assertEnhancedTrackingProtectionSheetStatus(status: String, state: Boolean) {
mDevice.waitNotNull(Until.findObjects(By.textContains(status)))
mDevice.waitNotNull(Until.findObjects(By.text("Protections are $status for this site")))
onView(ViewMatchers.withResourceName("switch_widget")).check(
matches(
isChecked(
@ -136,11 +237,7 @@ private fun assertEnhancedTrackingProtectionSheetStatus(status: String, state: B
)
}
private fun assertEnhancedTrackingProtectionDetailsStatus(status: String) {
mDevice.waitNotNull(Until.findObjects(By.textContains(status)))
}
private fun openEnhancedTrackingProtectionSheet() =
private fun pageSecurityIndicator() =
mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_security_indicator"))
private fun enhancedTrackingProtectionSwitch() =
@ -156,35 +253,45 @@ private fun trackingProtectionSettingsButton() =
private fun openEnhancedTrackingProtectionDetails() =
mDevice.findObject(UiSelector().resourceId("$packageName:id/trackingProtectionDetails"))
private fun assertTrackingCookiesBlocked() {
mDevice.findObject(UiSelector().resourceId("$packageName:id/cross_site_tracking"))
.waitForExists(waitingTime)
onView(withId(R.id.blocking_header)).check(matches(isDisplayed()))
onView(withId(R.id.tracking_content)).check(matches(isDisplayed()))
}
private val trackingContentBlockListButton =
onView(
allOf(
withText("Tracking Content"),
withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE),
),
)
private fun assertFingerprintersBlocked() {
mDevice.findObject(UiSelector().resourceId("$packageName:id/fingerprinters"))
.waitForExists(waitingTime)
onView(withId(R.id.blocking_header)).check(matches(isDisplayed()))
onView(withId(R.id.fingerprinters)).check(matches(isDisplayed()))
}
private val socialTrackersBlockListButton =
onView(
allOf(
withId(R.id.social_media_trackers),
withText("Social Media Trackers"),
),
)
private fun assertCryptominersBlocked() {
mDevice.findObject(UiSelector().resourceId("$packageName:id/cryptominers"))
.waitForExists(waitingTime)
onView(withId(R.id.blocking_header)).check(matches(isDisplayed()))
onView(withId(R.id.cryptominers)).check(matches(isDisplayed()))
}
private val crossSiteCookiesBlockListButton =
onView(
allOf(
withId(R.id.cross_site_tracking),
withText("Cross-Site Cookies"),
),
)
private fun assertTrackingContentBlocked() {
assertTrue(
mDevice.findObject(UiSelector().resourceId("$packageName:id/tracking_content"))
.waitForExists(waitingTime),
private val cryptominersBlockListButton =
onView(
allOf(
withId(R.id.cryptominers),
withText("Cryptominers"),
),
)
}
private fun trackingContentBlockListButton() = onView(withId(R.id.tracking_content))
private val fingerprintersBlockListButton =
onView(
allOf(
withId(R.id.fingerprinters),
withText("Fingerprinters"),
),
)
private fun assertSecuritySheetIsCompletelyDisplayed() {
mDevice.findObject(UiSelector().description(getStringResource(R.string.quick_settings_sheet)))

@ -7,7 +7,10 @@
package org.mozilla.fenix.ui.robots
import android.graphics.Bitmap
import android.view.View
import android.widget.EditText
import android.widget.TextView
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotSelected
@ -519,6 +522,21 @@ class HomeScreenRobot {
},
)
}
fun verifyNimbusMessageCard(title: String, text: String, action: String) {
val textView = UiSelector()
.className(ComposeView::class.java)
.className(View::class.java)
.className(TextView::class.java)
assertTrue(
mDevice.findObject(textView.textContains(title)).waitForExists(waitingTime),
)
assertTrue(
mDevice.findObject(textView.textContains(text)).waitForExists(waitingTime),
)
assertTrue(
mDevice.findObject(textView.textContains(action)).waitForExists(waitingTime),
)
}
class Transition {

@ -39,6 +39,7 @@ import org.mozilla.fenix.helpers.Constants
import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION
import org.mozilla.fenix.helpers.Constants.RETRY_COUNT
import org.mozilla.fenix.helpers.Constants.SPEECH_RECOGNITION
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
import org.mozilla.fenix.helpers.SessionLoadedIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
@ -212,10 +213,11 @@ class SearchRobot {
searchEnginesShortcutButton.click()
}
fun clickScanButton() {
scanButton.waitForExists(waitingTime)
scanButton.click()
}
fun clickScanButton() =
scanButton.also {
it.waitForExists(waitingTime)
it.click()
}
fun clickDismissPermissionRequiredDialog() {
dismissPermissionButton.waitForExists(waitingTime)
@ -399,7 +401,10 @@ private val goToPermissionsSettingsButton =
mDevice.findObject(UiSelector().text("GO TO SETTINGS"))
private val scanButton =
mDevice.findObject(UiSelector().resourceId("$packageName:id/qr_scan_button"))
itemWithResIdAndText(
"$packageName:id/qr_scan_button",
getStringResource(R.string.search_scan_button),
)
private fun clearButton() =
mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_clear_view"))
@ -510,18 +515,15 @@ private fun assertEngineListShortcutContains(rule: ComposeTestRule, searchEngine
).waitForExists(waitingTime)
rule.onNodeWithTag("mozac.awesomebar.suggestions")
.performScrollToIndex(5)
.performScrollToNode(hasText(searchEngineName))
rule.onNodeWithText(searchEngineName)
.assertExists()
.assertIsDisplayed()
.assertHasClickAction()
}
}
private fun ComposeTestRule.selectDefaultSearchEngine(searchEngine: String) {
onView(withId(R.id.mozac_browser_toolbar_edit_icon)).click()
onNodeWithText(searchEngine)
.assertExists()
.assertIsDisplayed()

@ -22,8 +22,11 @@ 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
@ -35,16 +38,21 @@ import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import junit.framework.AssertionFailedError
import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.endsWith
import org.hamcrest.Matchers.allOf
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.Constants.LISTS_MAXSWIPES
import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_PLAY_SERVICES
import org.mozilla.fenix.helpers.Constants.RETRY_COUNT
import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
import org.mozilla.fenix.helpers.TestHelper.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
@ -67,11 +75,30 @@ class SettingsRobot {
fun verifyThemeSelected() = assertThemeSelected()
fun verifyAccessibilityButton() = assertAccessibilityButton()
fun verifySetAsDefaultBrowserButton() = assertSetAsDefaultBrowserButton()
fun verifyTabsButton() = assertTabsButton()
fun verifyTabsButton() =
assertItemContainingTextExists(itemContainingText(getStringResource(R.string.preferences_tabs)))
fun verifyHomepageButton() = assertHomepageButton()
fun verifyAutofillButton() = assertAutofillButton()
fun verifyLanguageButton() = assertLanguageButton()
fun verifyDefaultBrowserIsDisabled() = assertDefaultBrowserIsDisabled()
fun verifyDefaultBrowserToggle(isEnabled: Boolean) {
scrollToElementByText(getStringResource(R.string.preferences_set_as_default_browser))
onView(withText(R.string.preferences_set_as_default_browser))
.check(
matches(
hasCousin(
allOf(
withId(R.id.switch_widget),
if (isEnabled) {
isChecked()
} else {
isNotChecked()
},
),
),
),
)
}
fun clickDefaultBrowserSwitch() = toggleDefaultBrowserSwitch()
fun verifyAndroidDefaultAppsMenuAppears() = assertAndroidDefaultAppsMenuAppears()
@ -79,21 +106,18 @@ class SettingsRobot {
fun verifyPrivacyHeading() = assertPrivacyHeading()
fun verifyHTTPSOnlyModeButton() = assertHTTPSOnlyModeButton()
fun verifyHTTPSOnlyModeState(state: String) = assertHTTPSOnlyModeState(state)
fun verifyCookieBannerReductionButton() =
onView(withText(R.string.preferences_cookie_banner_reduction)).check(matches(isDisplayed()))
fun verifyEnhancedTrackingProtectionButton() = assertEnhancedTrackingProtectionButton()
fun verifyLoginsAndPasswordsButton() = assertLoginsAndPasswordsButton()
fun verifyEnhancedTrackingProtectionState(option: String) =
assertEnhancedTrackingProtectionState(option)
fun verifyPrivateBrowsingButton() = assertPrivateBrowsingButton()
fun verifySitePermissionsButton() = assertSitePermissionsButton()
fun verifyDeleteBrowsingDataButton() = assertDeleteBrowsingDataButton()
fun verifyDeleteBrowsingDataOnQuitButton() = assertDeleteBrowsingDataOnQuitButton()
fun verifyDeleteBrowsingDataOnQuitState(state: String) =
assertDeleteBrowsingDataState(state)
fun verifyNotificationsButton() = assertNotificationsButton()
fun verifyDataCollectionButton() = assertDataCollectionButton()
fun verifyOpenLinksInAppsButton() = assertOpenLinksInAppsButton()
fun verifyOpenLinksInAppsState(state: String) = assertOpenLinksInAppsSwitchState(state)
fun verifySettingsView() = assertSettingsView()
fun verifySettingsToolbar() = assertSettingsToolbar()
@ -101,8 +125,64 @@ class SettingsRobot {
fun verifyAdvancedHeading() = assertAdvancedHeading()
fun verifyAddons() = assertAddonsButton()
fun verifyExternalDownloadManagerButton() =
onView(
withText(R.string.preferences_external_download_manager),
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
fun verifyExternalDownloadManagerToggle(enabled: Boolean) =
onView(withText(R.string.preferences_external_download_manager))
.check(
matches(
hasCousin(
allOf(
withClassName(endsWith("Switch")),
if (enabled) {
isChecked()
} else {
isNotChecked()
},
),
),
),
)
fun verifyLeakCanaryToggle(enabled: Boolean) =
onView(withText(R.string.preference_leakcanary))
.check(
matches(
hasCousin(
allOf(
withClassName(endsWith("Switch")),
if (enabled) {
isChecked()
} else {
isNotChecked()
},
),
),
),
)
fun verifyRemoteDebuggingToggle(enabled: Boolean) =
onView(withText(R.string.preferences_remote_debugging))
.check(
matches(
hasCousin(
allOf(
withClassName(endsWith("Switch")),
if (enabled) {
isChecked()
} else {
isNotChecked()
},
),
),
),
)
// DEVELOPER TOOLS SECTION
fun verifyRemoteDebug() = assertRemoteDebug()
fun verifyRemoteDebuggingButton() = assertRemoteDebuggingButton()
fun verifyLeakCanaryButton() = assertLeakCanaryButton()
// ABOUT SECTION
@ -112,6 +192,16 @@ class SettingsRobot {
fun verifyAboutFirefoxPreview() = assertTrue(aboutFirefoxHeading().waitForExists(waitingTime))
fun verifyGooglePlayRedirect() = assertGooglePlayRedirect()
fun verifySettingsOptionSummary(setting: String, summary: String) {
scrollToElementByText(setting)
onView(
allOf(
withText(setting),
hasSibling(withText(summary)),
),
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
class Transition {
fun goBack(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
goBackButton().click()
@ -152,8 +242,11 @@ class SettingsRobot {
}
fun openTabsSubMenu(interact: SettingsSubMenuTabsRobot.() -> Unit): SettingsSubMenuTabsRobot.Transition {
fun tabsButton() = onView(withText("Tabs"))
tabsButton().click()
itemWithText(getStringResource(R.string.preferences_tabs))
.also {
it.waitForExists(waitingTime)
it.clickAndWaitForNewWindow(waitingTimeShort)
}
SettingsSubMenuTabsRobot().interact()
return SettingsSubMenuTabsRobot.Transition()
@ -400,12 +493,6 @@ private fun assertSetAsDefaultBrowserButton() {
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertDefaultBrowserIsDisabled() {
scrollToElementByText("Set as default browser")
onView(withId(R.id.switch_widget))
.check(matches(ViewMatchers.isNotChecked()))
}
private fun toggleDefaultBrowserSwitch() {
scrollToElementByText("Privacy and security")
onView(withText("Set as default browser")).perform(ViewActions.click())
@ -415,12 +502,6 @@ private fun assertAndroidDefaultAppsMenuAppears() {
intended(IntentMatchers.hasAction(DEFAULT_APPS_SETTINGS_ACTION))
}
private fun assertTabsButton() {
mDevice.wait(Until.findObject(By.text("Tabs")), waitingTime)
onView(withText(R.string.preferences_tabs))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
// PRIVACY SECTION
private fun assertPrivacyHeading() {
scrollToElementByText("Privacy and security")
@ -435,15 +516,6 @@ private fun assertHTTPSOnlyModeButton() {
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertHTTPSOnlyModeState(state: String) {
onView(
allOf(
withText(R.string.preferences_https_only_title),
hasSibling(withText(state)),
),
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertEnhancedTrackingProtectionButton() {
mDevice.wait(Until.findObject(By.text("Privacy and Security")), waitingTime)
onView(withId(R.id.recycler_view)).perform(
@ -453,11 +525,6 @@ private fun assertEnhancedTrackingProtectionButton() {
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertEnhancedTrackingProtectionState(state: String) {
mDevice.wait(Until.findObject(By.text("Enhanced Tracking Protection")), waitingTime)
onView(withText(state)).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertLoginsAndPasswordsButton() {
scrollToElementByText("Logins and passwords")
onView(withText(R.string.preferences_passwords_logins_and_passwords))
@ -489,15 +556,6 @@ private fun assertDeleteBrowsingDataOnQuitButton() {
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertDeleteBrowsingDataState(state: String) {
onView(
allOf(
withText(R.string.preferences_delete_browsing_data_on_quit),
hasSibling(withText(state)),
),
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertNotificationsButton() {
scrollToElementByText("Notifications")
onView(withText("Notifications"))
@ -518,21 +576,6 @@ private fun assertOpenLinksInAppsButton() {
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
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
private fun assertDeveloperToolsHeading() {
scrollToElementByText("Developer tools")
onView(withText("Developer tools"))
}
// ADVANCED SECTION
private fun assertAdvancedHeading() {
onView(withId(R.id.recycler_view)).perform(
@ -556,7 +599,7 @@ private fun assertAddonsButton() {
.check(matches(isCompletelyDisplayed()))
}
private fun assertRemoteDebug() {
private fun assertRemoteDebuggingButton() {
scrollToElementByText("Remote debugging via USB")
onView(withText("Remote debugging via USB"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))

@ -407,11 +407,22 @@ class SettingsSubMenuAutofillRobot {
private val autofillToolbarTitle = itemContainingText(getStringResource(R.string.preferences_autofill))
private val addressesSectionTitle = itemContainingText(getStringResource(R.string.preferences_addresses))
private val manageAddressesToolbarTitle = itemContainingText(getStringResource(R.string.addresses_manage_addresses))
private val manageAddressesToolbarTitle =
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/navigationToolbar")
.childSelector(UiSelector().text(getStringResource(R.string.addresses_manage_addresses))),
)
private val saveAndAutofillAddressesOption = itemContainingText(getStringResource(R.string.preferences_addresses_save_and_autofill_addresses))
private val saveAndAutofillAddressesSummary = itemContainingText(getStringResource(R.string.preferences_addresses_save_and_autofill_addresses_summary))
private val addAddressButton = itemContainingText(getStringResource(R.string.preferences_addresses_add_address))
private val manageAddressesButton = itemContainingText(getStringResource(R.string.preferences_addresses_manage_addresses))
private val manageAddressesButton =
mDevice.findObject(
UiSelector()
.resourceId("android:id/title")
.text(getStringResource(R.string.preferences_addresses_manage_addresses)),
)
private val addAddressToolbarTitle = itemContainingText(getStringResource(R.string.addresses_add_address))
private val editAddressToolbarTitle = itemContainingText(getStringResource(R.string.addresses_edit_address))
private val toolbarCheckmarkButton = itemWithResId("$packageName:id/save_address_button")

@ -11,7 +11,9 @@ import org.mozilla.fenix.helpers.MatcherHelper.checkedItemWithResId
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
import org.mozilla.fenix.helpers.TestHelper.getStringResource
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click
/**
* Implementation of Robot Pattern for the settings Cookie Banner Reduction sub menu.
@ -25,7 +27,14 @@ class SettingsSubMenuCookieBannerReductionRobot {
fun verifyCheckedCookieBannerReductionToggle(isCookieBannerReductionChecked: Boolean) =
assertCheckedItemWithResIdExists(checkedCookieBannerOptionToggle(isCookieBannerReductionChecked))
class Transition
class Transition {
fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
mDevice.pressBack()
SettingsRobot().interact()
return SettingsRobot.Transition()
}
}
}
private val cookieBannerOptionTitle =

@ -6,16 +6,26 @@ package org.mozilla.fenix.ui.robots
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.RootMatchers
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isChecked
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.withParent
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.endsWith
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.assertIsEnabled
import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithDescriptionExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdExists
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
import org.mozilla.fenix.helpers.TestHelper.getStringResource
import org.mozilla.fenix.helpers.TestHelper.hasCousin
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click
/**
@ -23,24 +33,99 @@ import org.mozilla.fenix.helpers.click
*/
class SettingsSubMenuDataCollectionRobot {
fun verifyNavigationToolBarHeader() = assertNavigationToolBarHeader()
fun verifyDataCollectionOptions() = assertDataCollectionOptions()
fun verifyUsageAndTechnicalDataSwitchDefault() = assertUsageAndTechnicalDataSwitchDefault()
fun verifyDataCollectionView(
isUsageAndTechnicalDataEnabled: Boolean,
isMarketingDataEnabled: Boolean,
studiesSummary: String,
) {
assertItemWithDescriptionExists(goBackButton())
assertItemContainingTextExists(
itemContainingText(getStringResource(R.string.preferences_data_collection)),
itemContainingText(getStringResource(R.string.preference_usage_data)),
itemContainingText(getStringResource(R.string.preferences_usage_data_description)),
)
verifyUsageAndTechnicalDataToggle(isUsageAndTechnicalDataEnabled)
assertItemContainingTextExists(
itemContainingText(getStringResource(R.string.preferences_marketing_data)),
itemContainingText(getStringResource(R.string.preferences_marketing_data_description2)),
)
verifyMarketingDataToggle(isMarketingDataEnabled)
assertItemContainingTextExists(
itemContainingText(getStringResource(R.string.preference_experiments_2)),
itemContainingText(studiesSummary),
)
}
fun verifyMarketingDataSwitchDefault() = assertMarketingDataValueSwitchDefault()
fun verifyUsageAndTechnicalDataToggle(enabled: Boolean) =
onView(withText(R.string.preference_usage_data))
.check(
matches(
hasCousin(
allOf(
withClassName(endsWith("Switch")),
if (enabled) {
isChecked()
} else {
isNotChecked()
},
),
),
),
)
fun verifyMarketingDataToggle(enabled: Boolean) =
onView(withText(R.string.preferences_marketing_data))
.check(
matches(
hasCousin(
allOf(
withClassName(endsWith("Switch")),
if (enabled) {
isChecked()
} else {
isNotChecked()
},
),
),
),
)
fun verifyStudiesToggle(enabled: Boolean) =
onView(withId(R.id.studies_switch))
.check(
matches(
if (enabled) {
isChecked()
} else {
isNotChecked()
},
),
)
fun clickUsageAndTechnicalDataToggle() =
itemContainingText(getStringResource(R.string.preference_usage_data)).click()
fun clickMarketingDataToggle() =
itemContainingText(getStringResource(R.string.preferences_marketing_data)).click()
fun clickStudiesOption() =
itemContainingText(getStringResource(R.string.preference_experiments_2)).click()
fun clickStudiesToggle() =
itemWithResId("$packageName:id/studies_switch").click()
fun verifyStudiesDialog() {
assertItemWithResIdExists(itemWithResId("$packageName:id/alertTitle"))
assertItemContainingTextExists(
itemContainingText(getStringResource(R.string.studies_restart_app)),
)
studiesDialogOkButton.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
studiesDialogCancelButton.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
fun verifyExperimentsSwitchDefault() = assertExperimentsSwitchDefault()
fun clickStudiesDialogCancelButton() = studiesDialogCancelButton.click()
fun verifyDataCollectionSubMenuItems() {
verifyDataCollectionOptions()
verifyUsageAndTechnicalDataSwitchDefault()
verifyMarketingDataSwitchDefault()
// Temporarily disabled until https://github.com/mozilla-mobile/fenix/issues/17086 and
// https://github.com/mozilla-mobile/fenix/issues/17143 are resolved:
// verifyExperimentsSwitchDefault()
}
fun clickStudiesDialogOkButton() = studiesDialogOkButton.click()
class Transition {
fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
@ -52,53 +137,6 @@ class SettingsSubMenuDataCollectionRobot {
}
}
private fun goBackButton() =
onView(withContentDescription("Navigate up"))
private fun assertNavigationToolBarHeader() = onView(
allOf(
withParent(withId(R.id.navigationToolbar)),
withText(R.string.preferences_data_collection),
),
)
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertDataCollectionOptions() {
onView(withText(R.string.preference_usage_data))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
val usageAndTechnicalDataText =
"Shares performance, usage, hardware and customization data about your browser with Mozilla to help us make $appName better"
onView(withText(usageAndTechnicalDataText))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
onView(withText(R.string.preferences_marketing_data))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
val marketingDataText =
"Shares basic usage data with Adjust, our mobile marketing vendor"
onView(withText(marketingDataText))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
// Temporarily disabled until https://github.com/mozilla-mobile/fenix/issues/17086 and
// https://github.com/mozilla-mobile/fenix/issues/17143 are resolved:
// onView(withText(R.string.preference_experiments_2)).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
// onView(withText(R.string.preference_experiments_summary_2)).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun usageAndTechnicalDataButton() = onView(withText(R.string.preference_usage_data))
private fun assertUsageAndTechnicalDataSwitchDefault() = usageAndTechnicalDataButton()
.assertIsEnabled(isEnabled = true)
private fun marketingDataButton() = onView(withText(R.string.preferences_marketing_data))
private fun assertMarketingDataValueSwitchDefault() = marketingDataButton()
.assertIsEnabled(isEnabled = true)
private fun experimentsButton() = onView(withText(R.string.preference_experiments_2))
private fun assertExperimentsSwitchDefault() = experimentsButton()
.assertIsEnabled(isEnabled = true)
private fun goBackButton() = itemWithDescription("Navigate up")
private val studiesDialogOkButton = onView(withId(android.R.id.button1)).inRoot(RootMatchers.isDialog())
private val studiesDialogCancelButton = onView(withId(android.R.id.button2)).inRoot(RootMatchers.isDialog())

@ -14,9 +14,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers.allOf
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
@ -33,9 +31,6 @@ class SettingsSubMenuDeleteBrowsingDataRobot {
fun verifyNavigationToolBarHeader() = assertNavigationToolBarHeader()
fun verifyDeleteBrowsingDataButton() = assertDeleteBrowsingDataButton()
fun verifyMessageInDialogBox() = assertMessageInDialogBox()
fun verifyDeleteButtonInDialogBox() = assertDeleteButtonInDialogBox()
fun verifyCancelButtonInDialogBox() = assertCancelButtonInDialogBox()
fun verifyAllOptionsAndCheckBoxes() = assertAllOptionsAndCheckBoxes()
fun verifyAllCheckBoxesAreChecked() = assertAllCheckBoxesAreChecked()
fun verifyOpenTabsCheckBox(status: Boolean) = assertOpenTabsCheckBox(status)
@ -47,10 +42,10 @@ class SettingsSubMenuDeleteBrowsingDataRobot {
fun verifyOpenTabsDetails(tabNumber: String) = assertOpenTabsDescription(tabNumber)
fun verifyBrowsingHistoryDetails(addresses: String) = assertBrowsingHistoryDescription(addresses)
fun verifyDialogElements() {
verifyMessageInDialogBox()
verifyDeleteButtonInDialogBox()
verifyCancelButtonInDialogBox()
fun verifyDeleteBrowsingDataDialog() {
dialogMessage().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
dialogCancelButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
dialogDeleteButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
fun switchOpenTabsCheckBox() = clickOpenTabsCheckBox()
@ -138,27 +133,11 @@ class SettingsSubMenuDeleteBrowsingDataRobot {
assertDownloadsCheckBox(false)
}
fun clickCancelButtonInDialogBoxAndVerifyContentsInDialogBox() {
mDevice.wait(
Until.findObject(By.text("Delete browsing data")),
waitingTime,
)
clickDeleteBrowsingDataButton()
verifyDialogElements()
cancelButton().click()
}
fun confirmDeletionAndAssertSnackbar() {
dialogDeleteButton().click()
assertDeleteBrowsingDataSnackbar()
}
fun verifyDeleteBrowsingDataSubMenuItems() {
verifyDeleteBrowsingDataButton()
clickCancelButtonInDialogBoxAndVerifyContentsInDialogBox()
verifyAllOptionsAndCheckBoxes()
verifyAllCheckBoxesAreChecked()
}
class Transition {
fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
goBackButton().click()
@ -188,9 +167,6 @@ private fun assertNavigationToolBarHeader() =
private fun assertDeleteBrowsingDataButton() =
deleteBrowsingDataButton().check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
private fun cancelButton() =
mDevice.findObject(UiSelector().textStartsWith("CANCEL"))
private fun dialogDeleteButton() = onView(withText("Delete")).inRoot(isDialog())
private fun dialogCancelButton() = onView(withText("Cancel")).inRoot(isDialog())
@ -242,15 +218,6 @@ private fun dialogMessage() =
onView(withText("$appName will delete the selected browsing data."))
.inRoot(isDialog())
private fun assertMessageInDialogBox() =
dialogMessage().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertDeleteButtonInDialogBox() =
dialogDeleteButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertCancelButtonInDialogBox() =
dialogCancelButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertAllOptionsAndCheckBoxes() {
openTabsSubsection().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
openTabsDescription("0").check(matches(withEffectiveVisibility(Visibility.VISIBLE)))

@ -7,6 +7,8 @@ package org.mozilla.fenix.ui.robots
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.Visibility
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.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
@ -14,9 +16,14 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.uiautomator.UiSelector
import junit.framework.TestCase.assertTrue
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.not
import org.hamcrest.Matchers.contains
import org.junit.Assert.assertFalse
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click
/**
@ -26,15 +33,35 @@ class SettingsSubMenuEnhancedTrackingProtectionExceptionsRobot {
fun verifyNavigationToolBarHeader() = assertNavigationToolBarHeader()
fun verifyDefault() = assertExceptionDefault()
fun verifyTPExceptionsDefaultView() {
assertTrue(
mDevice.findObject(
UiSelector().text("Exceptions let you disable tracking protection for selected sites."),
).waitForExists(waitingTime),
)
learnMoreLink.check(matches(isDisplayed()))
}
fun verifyExceptionLearnMoreText() = assertExceptionLearnMoreText()
fun openExceptionsLearnMoreLink() = learnMoreLink.click()
fun verifyListedURL(url: String) = assertExceptionURL(url)
fun removeOneSiteException(siteHost: String) {
exceptionsList.waitForExists(waitingTime)
removeSiteExceptionButton(siteHost).click()
}
fun verifyEnhancedTrackingProtectionProtectionExceptionsSubMenuItems() {
verifyDefault()
verifyExceptionLearnMoreText()
fun verifySiteExceptionExists(siteUrl: String, shouldExist: Boolean) {
exceptionsList.waitForExists(waitingTime)
if (shouldExist) {
assertTrue(
mDevice.findObject(UiSelector().textContains(siteUrl))
.waitForExists(waitingTime),
)
} else {
assertFalse(
mDevice.findObject(UiSelector().textContains(siteUrl))
.waitForExists(waitingTime),
)
}
}
class Transition {
@ -46,7 +73,7 @@ class SettingsSubMenuEnhancedTrackingProtectionExceptionsRobot {
}
fun disableExceptions(interact: SettingsSubMenuEnhancedTrackingProtectionExceptionsRobot.() -> Unit): Transition {
disableExceptionsButton().click()
disableAllExceptionsButton().click()
SettingsSubMenuEnhancedTrackingProtectionExceptionsRobot().interact()
return Transition()
@ -62,26 +89,18 @@ private fun assertNavigationToolBarHeader() {
.check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
}
private fun assertExceptionDefault() =
assertTrue(
mDevice.findObject(
UiSelector().text("Exceptions let you disable tracking protection for selected sites."),
).waitForExists(waitingTime),
)
private val learnMoreLink = onView(withText("Learn more"))
private fun assertExceptionLearnMoreText() =
assertTrue(
mDevice.findObject(
UiSelector().text("Learn more"),
).waitForExists(waitingTime),
)
private fun disableAllExceptionsButton() =
onView(withId(R.id.removeAllExceptions)).click()
private fun assertExceptionURL(url: String) =
assertTrue(
mDevice.findObject(
UiSelector().textContains(url.replace("http://", "https://")),
).waitForExists(waitingTime),
private fun removeSiteExceptionButton(siteHost: String) =
onView(
allOf(
withContentDescription("Delete"),
hasSibling(withText(containsString(siteHost))),
),
)
private fun disableExceptionsButton() =
onView(withId(R.id.removeAllExceptions)).click()
private val exceptionsList =
mDevice.findObject(UiSelector().resourceId("$packageName:id/exceptions_list"))

@ -4,7 +4,6 @@
package org.mozilla.fenix.ui.robots
import androidx.preference.R
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBack
@ -22,10 +21,14 @@ import androidx.test.espresso.matcher.ViewMatchers.withParent
import androidx.test.espresso.matcher.ViewMatchers.withParentIndex
import androidx.test.espresso.matcher.ViewMatchers.withResourceName
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers.allOf
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.assertIsChecked
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.isChecked
import org.mozilla.fenix.helpers.isEnabled
@ -37,36 +40,102 @@ class SettingsSubMenuEnhancedTrackingProtectionRobot {
fun verifyNavigationToolBarHeader() = assertNavigationToolBarHeader()
fun verifyEnhancedTrackingProtectionHeader() = assertEnhancedTrackingProtectionHeader()
fun verifyEnhancedTrackingProtectionHeaderDescription() = assertEnhancedTrackingProtectionHeaderDescription()
fun verifyEnhancedTrackingProtectionSummary() = assertEnhancedTrackingProtectionSummary()
fun verifyLearnMoreText() = assertLearnMoreText()
fun verifyEnhancedTrackingProtectionTextWithSwitchWidget() = assertEnhancedTrackingProtectionTextWithSwitchWidget()
fun verifyEnhancedTrackingProtectionOptionsEnabled(enabled: Boolean = true) = assertEnhancedTrackingProtectionOptionsState(enabled)
fun verifyEnhancedTrackingProtectionOptionsEnabled(enabled: Boolean = true) {
onView(withText("Standard (default)"))
.check(matches(isEnabled(enabled)))
onView(withText("Strict"))
.check(matches(isEnabled(enabled)))
onView(withText("Custom"))
.check(matches(isEnabled(enabled)))
}
fun verifyTrackingProtectionSwitchEnabled() = assertTrackingProtectionSwitchEnabled()
fun switchEnhancedTrackingProtectionToggle() = onView(withResourceName("switch_widget")).click()
fun switchEnhancedTrackingProtectionToggle() = onView(
allOf(
withText("Enhanced Tracking Protection"),
hasSibling(withResourceName("checkbox")),
),
).click()
fun verifyStandardOptionDescription() {
onView(withText(R.string.preference_enhanced_tracking_protection_standard_description_5))
.check(matches(isDisplayed()))
onView(withContentDescription(R.string.preference_enhanced_tracking_protection_standard_info_button))
.check(matches(isDisplayed()))
}
fun verifyStrictOptionDescription() {
onView(withText(R.string.preference_enhanced_tracking_protection_strict_description_4))
.check(matches(isDisplayed()))
onView(withContentDescription(R.string.preference_enhanced_tracking_protection_strict_info_button))
.check(matches(isDisplayed()))
}
fun verifyCustomTrackingProtectionSettings() {
scrollToElementByText("Redirect Trackers")
onView(withText(R.string.preference_enhanced_tracking_protection_custom_description_2))
.check(matches(isDisplayed()))
onView(withContentDescription(R.string.preference_enhanced_tracking_protection_custom_info_button))
.check(matches(isDisplayed()))
cookiesCheckbox().check(matches(isDisplayed()))
cookiesDropDownMenuDefault().check(matches(isDisplayed()))
trackingContentCheckbox().check(matches(isDisplayed()))
trackingcontentDropDownDefault().check(matches(isDisplayed()))
cryptominersCheckbox().check(matches(isDisplayed()))
fingerprintersCheckbox().check(matches(isDisplayed()))
redirectTrackersCheckbox().check(matches(isDisplayed()))
}
fun verifyRadioButtonDefaults() = assertRadioButtonDefaults()
fun verifyWhatsBlockedByStandardETPInfo() {
onView(withContentDescription(R.string.preference_enhanced_tracking_protection_standard_info_button)).click()
blockedByStandardETPInfo()
}
fun verifyEnhancedTrackingProtectionProtectionSubMenuItems() {
verifyEnhancedTrackingProtectionHeader()
verifyEnhancedTrackingProtectionHeaderDescription()
verifyLearnMoreText()
verifyEnhancedTrackingProtectionTextWithSwitchWidget()
verifyTrackingProtectionSwitchEnabled()
verifyRadioButtonDefaults()
verifyEnhancedTrackingProtectionOptionsEnabled()
fun verifyWhatsBlockedByStrictETPInfo() {
onView(withContentDescription(R.string.preference_enhanced_tracking_protection_strict_info_button)).click()
// Repeating the info as in the standard option, with one extra point.
blockedByStandardETPInfo()
onView(withText("Tracking Content")).check(matches(isDisplayed()))
onView(withText("Stops outside ads, videos, and other content from loading that contains tracking code. May affect some website functionality.")).check(matches(isDisplayed()))
}
fun verifyCustomTrackingProtectionSettings() = assertCustomTrackingProtectionSettings()
fun verifyWhatsBlockedByCustomETPInfo() {
onView(withContentDescription(R.string.preference_enhanced_tracking_protection_custom_info_button)).click()
// Repeating the info as in the standard option, with one extra point.
blockedByStandardETPInfo()
onView(withText("Tracking Content")).check(matches(isDisplayed()))
onView(withText("Stops outside ads, videos, and other content from loading that contains tracking code. May affect some website functionality.")).check(matches(isDisplayed()))
}
fun selectTrackingProtectionOption(option: String) = onView(withText(option)).click()
fun verifyEnhancedTrackingProtectionLevelSelected(option: String, checked: Boolean) {
mDevice.wait(
Until.findObject(By.text("Enhanced Tracking Protection")),
waitingTime,
)
onView(withText(option))
.check(
matches(
hasSibling(
allOf(
withId(R.id.radio_button),
isChecked(checked),
),
),
),
)
}
class Transition {
fun goBackToHomeScreen(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
// To settings
@ -105,36 +174,21 @@ class SettingsSubMenuEnhancedTrackingProtectionRobot {
private fun assertNavigationToolBarHeader() {
onView(
allOf(
withParent(withId(org.mozilla.fenix.R.id.navigationToolbar)),
withParent(withId(R.id.navigationToolbar)),
withText("Enhanced Tracking Protection"),
),
)
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertEnhancedTrackingProtectionHeader() {
onView(withText("Browse without being followed"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertEnhancedTrackingProtectionHeaderDescription() {
onView(
allOf(
withParent(withParentIndex(0)),
withText("Keep your data to yourself. $appName protects you from many of the most common trackers that follow what you do online."),
),
)
private fun assertEnhancedTrackingProtectionSummary() {
onView(withText("$appName protects you from many of the most common trackers that follow what you do online."))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertLearnMoreText() {
onView(
allOf(
withParent(withParentIndex(0)),
withText("Learn more"),
),
)
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
onView(withText("Learn more"))
.check(matches(isDisplayed()))
}
private fun assertEnhancedTrackingProtectionTextWithSwitchWidget() {
@ -147,28 +201,8 @@ private fun assertEnhancedTrackingProtectionTextWithSwitchWidget() {
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertEnhancedTrackingProtectionOptionsState(enabled: Boolean) {
onView(withText("Standard (default)"))
.check(matches(isEnabled(enabled)))
onView(withText(org.mozilla.fenix.R.string.preference_enhanced_tracking_protection_standard_description_4))
.check(matches(isEnabled(enabled)))
onView(withText("Strict"))
.check(matches(isEnabled(enabled)))
onView(withText(org.mozilla.fenix.R.string.preference_enhanced_tracking_protection_strict_description_3))
.check(matches(isEnabled(enabled)))
onView(withText("Custom"))
.check(matches(isEnabled(enabled)))
onView(withText(org.mozilla.fenix.R.string.preference_enhanced_tracking_protection_custom_description_2))
.check(matches(isEnabled(enabled)))
}
private fun assertTrackingProtectionSwitchEnabled() {
onView(withResourceName("switch_widget")).check(
onView(withResourceName("checkbox")).check(
matches(
isChecked(
true,
@ -177,23 +211,6 @@ private fun assertTrackingProtectionSwitchEnabled() {
)
}
private fun assertRadioButtonDefaults() {
onView(
withText("Strict"),
).assertIsChecked(false)
onView(
allOf(
withId(org.mozilla.fenix.R.id.radio_button),
hasSibling(withText("Standard (default)")),
),
).assertIsChecked(true)
onView(
withText("Custom"),
).assertIsChecked(false)
}
fun settingsSubMenuEnhancedTrackingProtection(interact: SettingsSubMenuEnhancedTrackingProtectionRobot.() -> Unit): SettingsSubMenuEnhancedTrackingProtectionRobot.Transition {
SettingsSubMenuEnhancedTrackingProtectionRobot().interact()
return SettingsSubMenuEnhancedTrackingProtectionRobot.Transition()
@ -205,17 +222,6 @@ private fun goBackButton() =
private fun openExceptions() =
onView(allOf(withText("Exceptions")))
private fun assertCustomTrackingProtectionSettings() {
scrollToElementByText("Redirect Trackers")
cookiesCheckbox().check(matches(isDisplayed()))
cookiesDropDownMenuDefault().check(matches(isDisplayed()))
trackingContentCheckbox().check(matches(isDisplayed()))
trackingcontentDropDownDefault().check(matches(isDisplayed()))
cryptominersCheckbox().check(matches(isDisplayed()))
fingerprintersCheckbox().check(matches(isDisplayed()))
redirectTrackersCheckbox().check(matches(isDisplayed()))
}
private fun cookiesCheckbox() = onView(withText("Cookies"))
private fun cookiesDropDownMenuDefault() = onView(withText("Isolate cross-site cookies"))
@ -229,3 +235,16 @@ private fun cryptominersCheckbox() = onView(withText("Cryptominers"))
private fun fingerprintersCheckbox() = onView(withText("Fingerprinters"))
private fun redirectTrackersCheckbox() = onView(withText("Redirect Trackers"))
private fun blockedByStandardETPInfo() {
onView(withText("Social Media Trackers")).check(matches(isDisplayed()))
onView(withText("Limits the ability of social networks to track your browsing activity around the web.")).check(matches(isDisplayed()))
onView(withText("Cross-Site Cookies")).check(matches(isDisplayed()))
onView(withText("Total Cookie Protection isolates cookies to the site youre on so trackers like ad networks cant use them to follow you across sites.")).check(matches(isDisplayed()))
onView(withText("Cryptominers")).check(matches(isDisplayed()))
onView(withText("Prevents malicious scripts gaining access to your device to mine digital currency.")).check(matches(isDisplayed()))
onView(withText("Fingerprinters")).check(matches(isDisplayed()))
onView(withText("Stops uniquely identifiable data from being collected about your device that can be used for tracking purposes.")).check(matches(isDisplayed()))
onView(withText("Redirect Trackers")).check(matches(isDisplayed()))
onView(withText("Clears cookies set by redirects to known tracking websites.")).check(matches(isDisplayed()))
}

@ -26,6 +26,7 @@ import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.isChecked
/**
* Implementation of Robot Pattern for the settings Homepage sub menu.
@ -61,6 +62,14 @@ class SettingsSubMenuHomepageRobot {
assertHomepageAfterFourHoursButton()
}
fun verifySelectedOpeningScreenOption(openingScreenOption: String) =
onView(
allOf(
withId(R.id.radio_button),
hasSibling(withText(openingScreenOption)),
),
).check(matches(isChecked(true)))
fun clickShortcutsButton() = shortcutsButton().click()
fun clickSponsoredShortcuts() = sponsoredShortcutsButton().click()
@ -75,9 +84,13 @@ class SettingsSubMenuHomepageRobot {
fun clickPocketButton() = pocketButton().click()
fun clickStartOnHomepageButton() = homepageButton().click()
fun clickStartOnLastTabButton() = lastTabButton().click()
fun clickOpeningScreenOption(openingScreenOption: String) {
when (openingScreenOption) {
"Homepage" -> homepageButton().click()
"Last tab" -> lastTabButton().click()
"Homepage after four hours of inactivity" -> homepageAfterFourHoursButton().click()
}
}
fun openWallpapersMenu() = wallpapersMenuButton.click()
@ -96,13 +109,20 @@ class SettingsSubMenuHomepageRobot {
class Transition {
fun goBack(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
fun goBackToHomeScreen(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
goBackButton().click()
HomeScreenRobot().interact()
return HomeScreenRobot.Transition()
}
fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
goBackButton().click()
SettingsRobot().interact()
return SettingsRobot.Transition()
}
fun clickSnackBarViewButton(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
val snackBarButton = mDevice.findObject(UiSelector().text("VIEW"))
snackBarButton.waitForExists(waitingTimeShort)

@ -174,6 +174,13 @@ class SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot {
return SettingsSubMenuLoginsAndPasswordRobot.Transition()
}
fun goBackToSavedLogins(interact: SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.() -> Unit): SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.Transition {
goBackButton().perform(ViewActions.click())
SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot().interact()
return SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.Transition()
}
fun goToSavedWebsite(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
openWebsiteButton.click()

@ -5,32 +5,67 @@
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.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
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.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.TestHelper.getStringResource
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.isChecked
/**
* Implementation of Robot Pattern for the Open Links In Apps sub menu.
*/
class SettingsSubMenuOpenLinksInAppsRobot {
fun verifyOpenLinksInAppsView(selectedOpenLinkInAppsOption: String) {
assertItemWithDescriptionExists(goBackButton)
assertItemContainingTextExists(
itemContainingText(getStringResource(R.string.preferences_open_links_in_apps)),
itemContainingText(getStringResource(R.string.preferences_open_links_in_apps_always)),
itemContainingText(getStringResource(R.string.preferences_open_links_in_apps_ask)),
itemContainingText(getStringResource(R.string.preferences_open_links_in_apps_never)),
)
verifySelectedOpenLinksInAppOption(selectedOpenLinkInAppsOption)
}
fun verifySelectedOpenLinksInAppOption(openLinkInAppsOption: String) =
onView(
allOf(
withId(R.id.radio_button),
hasSibling(withText(openLinkInAppsOption)),
),
).check(matches(isChecked(true)))
fun clickOpenLinkInAppOption(openLinkInAppsOption: String) {
when (openLinkInAppsOption) {
"Always" -> alwaysOption.click()
"Ask before opening" -> askBeforeOpeningOption.click()
"Never" -> neverOption.click()
}
}
class Transition {
fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
mDevice.waitForIdle()
goBackButton().perform(ViewActions.click())
goBackButton.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")))
private val goBackButton = itemWithDescription("Navigate up")
private val alwaysOption =
itemContainingText(getStringResource(R.string.preferences_open_links_in_apps_always))
private val askBeforeOpeningOption =
itemContainingText(getStringResource(R.string.preferences_open_links_in_apps_ask))
private val neverOption =
itemContainingText(getStringResource(R.string.preferences_open_links_in_apps_never))

@ -7,6 +7,8 @@ package org.mozilla.fenix.ui.robots
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
@ -16,6 +18,10 @@ import org.hamcrest.CoreMatchers.allOf
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.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
import org.mozilla.fenix.helpers.TestHelper.getStringResource
@ -39,11 +45,13 @@ class SettingsSubMenuSitePermissionsCommonRobot {
fun verifyCheckAutoPlayRadioButtonDefault() = assertCheckAutoPayRadioButtonDefault()
fun verifyassertAskToAllowRecommended() = assertAskToAllowRecommended()
fun verifyAskToAllowButton(isChecked: Boolean = true) =
onView(withId(R.id.ask_to_allow_radio))
.check((matches(isDisplayed()))).assertIsChecked(isChecked)
fun verifyassertBlocked() = assertBlocked()
fun verifyCheckCommonRadioButtonDefault() = assertCheckCommonRadioButtonDefault()
fun verifyBlockedButton(isChecked: Boolean = false) =
onView(withId(R.id.block_radio))
.check((matches(isDisplayed()))).assertIsChecked(isChecked)
fun verifyBlockedByAndroid() = assertBlockedByAndroid()
@ -67,9 +75,8 @@ class SettingsSubMenuSitePermissionsCommonRobot {
}
fun verifySitePermissionsCommonSubMenuItems() {
verifyassertAskToAllowRecommended()
verifyassertBlocked()
verifyCheckCommonRadioButtonDefault()
verifyAskToAllowButton()
verifyBlockedButton()
verifyBlockedByAndroid()
verifyToAllowIt()
verifyGotoAndroidSettings()
@ -77,16 +84,15 @@ class SettingsSubMenuSitePermissionsCommonRobot {
verifyGoToSettingsButton()
}
fun verifySitePermissionsNotificationSubMenuItems() {
verifyassertAskToAllowRecommended()
verifyassertBlocked()
verifyCheckCommonRadioButtonDefault()
fun verifyNotificationSubMenuItems() {
verifyNotificationToolbar()
verifyAskToAllowButton()
verifyBlockedButton()
}
fun verifySitePermissionsPersistentStorageSubMenuItems() {
verifyassertAskToAllowRecommended()
verifyassertBlocked()
verifyCheckCommonRadioButtonDefault()
verifyAskToAllowButton()
verifyBlockedButton()
}
fun clickGoToSettingsButton() {
@ -143,6 +149,11 @@ class SettingsSubMenuSitePermissionsCommonRobot {
)
}
fun verifyNotificationToolbar() {
assertItemContainingTextExists(itemContainingText(getStringResource(R.string.preference_phone_feature_notification)))
assertItemWithDescriptionExists(itemWithDescription(getStringResource(R.string.action_bar_up_description)))
}
class Transition {
fun goBack(interact: SettingsSubMenuSitePermissionsRobot.() -> Unit): SettingsSubMenuSitePermissionsRobot.Transition {
goBackButton().click()
@ -183,17 +194,6 @@ private fun assertCheckAutoPayRadioButtonDefault() {
.assertIsChecked(isChecked = false)
}
private fun assertAskToAllowRecommended() = onView(withId(R.id.ask_to_allow_radio))
.check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
private fun assertBlocked() = onView(withId(R.id.block_radio))
.check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
private fun assertCheckCommonRadioButtonDefault() {
onView(withId(R.id.ask_to_allow_radio)).assertIsChecked(isChecked = true)
onView(withId(R.id.block_radio)).assertIsChecked(isChecked = false)
}
private fun assertBlockedByAndroid() {
blockedByAndroidContainer().waitForExists(waitingTime)
assertTrue(

@ -11,11 +11,13 @@ import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
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.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.CoreMatchers.allOf
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.click
/**
@ -23,9 +25,21 @@ import org.mozilla.fenix.helpers.click
*/
class SettingsSubMenuSitePermissionsRobot {
fun verifyNavigationToolBarHeader() = assertNavigationToolBarHeader()
fun verifySitePermissionsToolbarTitle() =
onView(withText("Site permissions")).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
fun verifySitePermissionsSubMenuItems() = assertSitePermissionsSubMenuItems()
fun verifyToolbarGoBackButton() =
goBackButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
fun verifySitePermissionOption(option: String, summary: String = "") {
scrollToElementByText(option)
onView(
allOf(
withText(option),
hasSibling(withText(summary)),
),
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
class Transition {
fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
@ -140,69 +154,6 @@ class SettingsSubMenuSitePermissionsRobot {
return SettingsSubMenuSitePermissionsExceptionsRobot.Transition()
}
}
private fun assertNavigationToolBarHeader() = onView(withText("Site permissions"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertSitePermissionsSubMenuItems() {
onView(withText("Autoplay"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
val autoplayText = "Block audio only"
onView(withText(autoplayText))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
val cameraText =
"Blocked by Android"
onView(withId(R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
allOf(hasDescendant(withText("Camera")), hasDescendant(withText(cameraText))),
),
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
val locationText =
"Blocked by Android"
onView(withId(R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
allOf(hasDescendant(withText("Location")), hasDescendant(withText(locationText))),
),
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
val microphoneText =
"Blocked by Android"
onView(withId(R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
allOf(hasDescendant(withText("Microphone")), hasDescendant(withText(microphoneText))),
),
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
onView(withText("Notification"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
val notificationText =
"Ask to allow"
onView(withId(R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
allOf(hasDescendant(withText("Notification")), hasDescendant(withText(notificationText))),
),
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
onView(withText("Persistent Storage"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
val persistentStorageText =
"Ask to allow"
onView(withId(R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
allOf(
hasDescendant(withText("Persistent Storage")),
hasDescendant(withText(persistentStorageText)),
),
),
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
}
private fun goBackButton() =

@ -11,11 +11,15 @@ import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
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.mDevice
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.isChecked
/**
* Implementation of Robot Pattern for the settings Tabs sub menu.
@ -28,6 +32,23 @@ class SettingsSubMenuTabsRobot {
fun verifyMoveOldTabsToInactiveOptions() = assertMoveOldTabsToInactiveOptions()
fun verifySelectedCloseTabsOption(closedTabsOption: String) =
onView(
allOf(
withId(R.id.radio_button),
hasSibling(withText(closedTabsOption)),
),
).check(matches(isChecked(true)))
fun clickClosedTabsOption(closedTabsOption: String) {
when (closedTabsOption) {
"Never" -> neverOption().click()
"After one day" -> afterOneDayOption().click()
"After one week" -> afterOneWeekOption().click()
"After one month" -> afterOneMonthOption().click()
}
}
class Transition {
fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
mDevice.waitForIdle()
@ -51,13 +72,13 @@ private fun assertTabViewOptions() {
private fun assertCloseTabsOptions() {
closeTabsHeading()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
neverToggle()
neverOption()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
afterOneDayToggle()
afterOneDayOption()
.check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
afterOneWeekToggle()
afterOneWeekOption()
.check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
afterOneMonthToggle()
afterOneMonthOption()
.check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
@ -78,13 +99,13 @@ private fun closeTabsHeading() = onView(withText("Close tabs"))
private fun manuallyToggle() = onView(withText("Manually"))
private fun neverToggle() = onView(withText("Never"))
private fun neverOption() = onView(withText("Never"))
private fun afterOneDayToggle() = onView(withText("After one day"))
private fun afterOneDayOption() = onView(withText("After one day"))
private fun afterOneWeekToggle() = onView(withText("After one week"))
private fun afterOneWeekOption() = onView(withText("After one week"))
private fun afterOneMonthToggle() = onView(withText("After one month"))
private fun afterOneMonthOption() = onView(withText("After one month"))
private fun moveOldTabsToInactiveHeading() = onView(withText("Move old tabs to inactive"))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 36 KiB

@ -48,6 +48,10 @@
android:usesCleartextTraffic="true"
tools:ignore="UnusedAttribute">
<profileable
android:shell="true"
tools:targetApi="29" />
<!--
We inherited this entry (${applicationId}.App) from Fennec. We need to keep this as our
main launcher to avoid launcher icons on the home screen disappearing for all our users.
@ -283,7 +287,7 @@
android:theme="@style/DialogActivityTheme" />
<activity
android:name=".gleanplumb.NotificationClickedReceiverActivity"
android:name=".messaging.NotificationClickedReceiverActivity"
android:exported="false" />
<service
@ -332,7 +336,7 @@
android:exported="false" />
<service
android:name=".gleanplumb.NotificationDismissedService"
android:name=".messaging.NotificationDismissedService"
android:exported="false" />
<service

@ -67,7 +67,7 @@ enum class ReleaseChannel {
object Config {
val channel = when (BuildConfig.BUILD_TYPE) {
"debug" -> ReleaseChannel.Debug
"nightly" -> ReleaseChannel.Nightly
"nightly", "benchmark" -> ReleaseChannel.Nightly
"beta" -> ReleaseChannel.Beta
"release" -> ReleaseChannel.Release
"forkDebug" -> ReleaseChannel.ForkDebug

@ -51,6 +51,7 @@ import mozilla.components.service.glean.net.ConceptFetchHttpUploader
import mozilla.components.support.base.facts.register
import mozilla.components.support.base.log.Log
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.ktx.android.arch.lifecycle.addObservers
import mozilla.components.support.ktx.android.content.isMainProcess
import mozilla.components.support.ktx.android.content.runOnlyInMainProcess
import mozilla.components.support.locale.LocaleAwareApplication
@ -85,6 +86,7 @@ import org.mozilla.fenix.ext.isKnownSearchDomain
import org.mozilla.fenix.ext.isNotificationChannelEnabled
import org.mozilla.fenix.ext.setCustomEndpointIfAvailable
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.lifecycle.StoreLifecycleObserver
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.onboarding.FenixOnboarding
import org.mozilla.fenix.onboarding.MARKETING_CHANNEL_ID
@ -278,7 +280,13 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
components.startupActivityLog.registerInAppOnCreate(this)
initVisualCompletenessQueueAndQueueTasks()
ProcessLifecycleOwner.get().lifecycle.addObserver(TelemetryLifecycleObserver(components.core.store))
ProcessLifecycleOwner.get().lifecycle.addObservers(
TelemetryLifecycleObserver(components.core.store),
StoreLifecycleObserver(
appStore = components.appStore,
browserStore = components.core.store,
),
)
components.analytics.metricsStorage.tryRegisterAsUsageRecorder(this)
@ -510,7 +518,8 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
}
}
private fun restoreMessaging() {
@VisibleForTesting
internal fun restoreMessaging() {
if (settings().isExperimentationEnabled) {
components.appStore.dispatch(AppAction.MessagingAction.Restore)
}
@ -732,6 +741,12 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
searchWidgetInstalled.set(settings.searchWidgetInstalled)
if (settings.sharedPrefsUUID.isEmpty()) {
settings.sharedPrefsUUID = sharedPrefsUuid.generateAndSet().toString()
} else {
sharedPrefsUuid.set(UUID.fromString(settings.sharedPrefsUUID))
}
val openTabsCount = settings.openTabsCount
hasOpenTabs.set(openTabsCount > 0)
if (openTabsCount > 0) {

@ -41,6 +41,7 @@ import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.NavigationUI
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -72,6 +73,7 @@ import mozilla.components.support.ktx.android.content.share
import mozilla.components.support.ktx.kotlin.isUrl
import mozilla.components.support.ktx.kotlin.toNormalizedUrl
import mozilla.components.support.locale.LocaleAwareAppCompatActivity
import mozilla.components.support.utils.BootUtils
import mozilla.components.support.utils.ManufacturerCodes
import mozilla.components.support.utils.SafeIntent
import mozilla.components.support.utils.toSafeIntent
@ -89,6 +91,7 @@ import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.components.metrics.BreadcrumbsRecorder
import org.mozilla.fenix.databinding.ActivityHomeBinding
import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExceptionsFragmentDirections
import org.mozilla.fenix.experiments.ResearchSurfaceDialogFragment
import org.mozilla.fenix.ext.alreadyOnDestination
import org.mozilla.fenix.ext.areNotificationsEnabledSafe
import org.mozilla.fenix.ext.breadcrumb
@ -97,7 +100,6 @@ import org.mozilla.fenix.ext.hasTopDestination
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.setNavigationIcon
import org.mozilla.fenix.ext.settings
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
@ -112,6 +114,9 @@ import org.mozilla.fenix.library.bookmarks.DesktopFolders
import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentDirections
import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections
import org.mozilla.fenix.messaging.FenixMessageSurfaceId
import org.mozilla.fenix.messaging.FenixNimbusMessagingController
import org.mozilla.fenix.messaging.MessageNotificationWorker
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.onboarding.FenixOnboarding
import org.mozilla.fenix.onboarding.ReEngagementNotificationWorker
@ -266,20 +271,35 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
it.start()
}
// Unless the activity is recreated, navigate to home first (without rendering it)
// to add it to the back stack.
if (savedInstanceState == null) {
navigateToHome()
}
if (!shouldStartOnHome() && shouldNavigateToBrowserOnColdStart(savedInstanceState)) {
navigateToBrowserOnColdStart()
if (settings().shouldShowJunoOnboarding(
hasUserBeenOnboarded = onboarding.userHasBeenOnboarded(),
isLauncherIntent = intent.toSafeIntent().isLauncherIntent,
)
) {
// Unless activity is recreated due to config change, navigate to onboarding
if (savedInstanceState == null) {
navHost.navController.navigate(NavGraphDirections.actionGlobalHomeJunoOnboarding())
}
} else {
StartOnHome.enterHomeScreen.record(NoExtras())
}
lifecycleScope.launch(IO) {
showFullscreenMessageIfNeeded(applicationContext)
}
if (settings().showHomeOnboardingDialog && onboarding.userHasBeenOnboarded()) {
navHost.navController.navigate(NavGraphDirections.actionGlobalHomeOnboardingDialog())
// Unless the activity is recreated, navigate to home first (without rendering it)
// to add it to the back stack.
if (savedInstanceState == null) {
navigateToHome()
}
if (!shouldStartOnHome() && shouldNavigateToBrowserOnColdStart(savedInstanceState)) {
navigateToBrowserOnColdStart()
} else {
StartOnHome.enterHomeScreen.record(NoExtras())
}
if (settings().showHomeOnboardingDialog && onboarding.userHasBeenOnboarded()) {
navHost.navController.navigate(NavGraphDirections.actionGlobalHomeOnboardingDialog())
}
showNotificationPermissionPromptIfRequired()
}
Performance.processIntentIfPerformanceTest(intent, this)
@ -341,8 +361,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
}
}
showNotificationPermissionPromptIfRequired()
components.backgroundServices.accountManagerAvailableQueue.runIfReadyOrQueue {
lifecycleScope.launch(IO) {
// If we're authenticated, kick-off a sync and a device state refresh.
@ -357,6 +375,9 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
startTimeProfiler,
"HomeActivity.onCreate",
)
components.notificationsDelegate.bindToActivity(this)
StartupTimeline.onActivityCreateEndHome(this) // DO NOT MOVE ANYTHING BELOW HERE.
}
@ -365,6 +386,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
* Show the pre permission dialog to the user once if the notification are not enabled.
*/
private fun showNotificationPermissionPromptIfRequired() {
if (settings().junoOnboardingEnabled) {
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
!NotificationManagerCompat.from(applicationContext).areNotificationsEnabledSafe() &&
settings().numberOfAppLaunches <= 1
@ -427,11 +452,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
Events.defaultBrowserChanged.record(NoExtras())
}
// We attempt to send metrics onResume so that the start of new user sessions is not
// missed. Previously, this was done in FenixApplication::onCreate, but it was decided
// that we should not rely on the application being killed between user sessions.
components.appStore.dispatch(AppAction.ResumedMetricsAction)
ReEngagementNotificationWorker.setReEngagementNotificationIfNeeded(applicationContext)
MessageNotificationWorker.setMessageNotificationWorker(applicationContext)
}
@ -559,6 +579,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.core.pocketStoriesService.stopPeriodicStoriesRefresh()
components.core.pocketStoriesService.stopPeriodicSponsoredStoriesRefresh()
privateNotificationObserver?.stop()
components.notificationsDelegate.unBindActivity(this)
}
override fun onConfigurationChanged(newConfig: Configuration) {
@ -1162,6 +1183,49 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
!processIntent(intent)
}
private suspend fun showFullscreenMessageIfNeeded(context: Context) {
val messagingStorage = context.components.analytics.messagingStorage
val messages = messagingStorage.getMessages()
val nextMessage =
messagingStorage.getNextMessage(FenixMessageSurfaceId.SURVEY, messages)
?: return
val fenixNimbusMessagingController = FenixNimbusMessagingController(messagingStorage)
val researchSurfaceDialogFragment = ResearchSurfaceDialogFragment.newInstance(
keyMessageText = nextMessage.data.text,
keyAcceptButtonText = nextMessage.data.buttonLabel,
keyDismissButtonText = null,
)
researchSurfaceDialogFragment.onAccept = {
processIntent(fenixNimbusMessagingController.getIntentForMessage(nextMessage))
components.appStore.dispatch(AppAction.MessagingAction.MessageClicked(nextMessage))
}
researchSurfaceDialogFragment.onDismiss = {
components.appStore.dispatch(AppAction.MessagingAction.MessageDismissed(nextMessage))
}
lifecycleScope.launch(Main) {
researchSurfaceDialogFragment.showNow(
supportFragmentManager,
ResearchSurfaceDialogFragment.FRAGMENT_TAG,
)
}
// Update message as displayed.
val currentBootUniqueIdentifier = BootUtils.getBootIdentifier(context)
val updatedMessage =
fenixNimbusMessagingController.updateMessageAsDisplayed(
nextMessage,
currentBootUniqueIdentifier,
)
fenixNimbusMessagingController.onMessageDisplayed(updatedMessage)
return
}
companion object {
const val OPEN_TO_BROWSER = "open_to_browser"
const val OPEN_TO_BROWSER_AND_LOAD = "open_to_browser_and_load"

@ -100,9 +100,11 @@ import mozilla.components.support.ktx.android.view.hideKeyboard
import mozilla.components.support.ktx.kotlin.getOrigin
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import mozilla.components.support.locale.ActivityContextWrapper
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.GleanMetrics.MediaState
import org.mozilla.fenix.GleanMetrics.PullToRefreshInBrowser
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.IntentReceiverActivity
import org.mozilla.fenix.NavGraphDirections
@ -251,6 +253,8 @@ abstract class BaseBrowserFragment :
val activity = activity as HomeActivity
activity.themeManager.applyStatusBarTheme(activity)
val originalContext = ActivityContextWrapper.getOriginalContext(activity)
binding.engineView.setActivityContext(originalContext)
browserFragmentStore = StoreProvider.get(this) {
BrowserFragmentStore(
@ -495,8 +499,9 @@ abstract class BaseBrowserFragment :
httpClient = context.components.core.client,
store = store,
tabId = customTabSessionId,
snackbarParent = binding.dynamicSnackbarContainer,
snackbarDelegate = FenixSnackbarDelegate(binding.dynamicSnackbarContainer),
onCopyConfirmation = {
showSnackbarForClipboardCopy()
},
)
val downloadFeature = DownloadsFeature(
@ -509,6 +514,7 @@ abstract class BaseBrowserFragment :
context.applicationContext,
store,
DownloadService::class,
notificationsDelegate = context.components.notificationsDelegate,
),
shouldForwardToThirdParties = {
PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
@ -877,6 +883,7 @@ abstract class BaseBrowserFragment :
requireComponents.core.store,
context.components.useCases.sessionUseCases.reload,
binding.swipeRefresh,
{ PullToRefreshInBrowser.executed.record(NoExtras()) },
customTabSessionId,
),
owner = this,
@ -900,6 +907,22 @@ abstract class BaseBrowserFragment :
initializeEngineView(toolbarHeight)
}
/**
* Show a [Snackbar] when data is set to the device clipboard. To avoid duplicate displays of
* information only show a [Snackbar] for Android 12 and lower.
*
* [See details](https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications).
*/
private fun showSnackbarForClipboardCopy() {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
FenixSnackbarDelegate(binding.dynamicSnackbarContainer).show(
snackBarParentView = binding.dynamicSnackbarContainer,
text = R.string.snackbar_copy_image_to_clipboard_confirmation,
duration = Snackbar.LENGTH_LONG,
)
}
}
/**
* Shows a biometric prompt and fallback to prompting for the password.
*/
@ -1491,7 +1514,9 @@ abstract class BaseBrowserFragment :
message = "onDestroyView()",
)
binding.engineView.setActivityContext(null)
requireContext().accessibilityManager.removeAccessibilityStateChangeListener(this)
_browserToolbarView = null
_browserToolbarInteractor = null
_binding = null

@ -49,6 +49,7 @@ import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.settings.quicksettings.protections.cookiebanners.dialog.CookieBannerReEngagementDialogUtils
import org.mozilla.fenix.settings.quicksettings.protections.cookiebanners.getCookieBannerUIMode
import org.mozilla.fenix.shortcut.PwaOnboardingObserver
import org.mozilla.fenix.theme.ThemeManager
@ -377,36 +378,31 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
val useCase = requireComponents.useCases.trackingProtectionUseCases
FxNimbus.features.cookieBanners.recordExposure()
useCase.containsException(tab.id) { hasTrackingProtectionException ->
lifecycleScope.launch(Dispatchers.Main) {
lifecycleScope.launch {
val cookieBannersStorage = requireComponents.core.cookieBannersStorage
val hasCookieBannerException =
if (requireContext().settings().shouldUseCookieBanner) {
withContext(Dispatchers.IO) {
cookieBannersStorage.hasException(
tab.content.url,
tab.content.private,
val cookieBannerUIMode = cookieBannersStorage.getCookieBannerUIMode(
requireContext(),
tab,
)
withContext(Dispatchers.Main) {
runIfFragmentIsAttached {
val isTrackingProtectionEnabled =
tab.trackingProtection.enabled && !hasTrackingProtectionException
val directions =
BrowserFragmentDirections.actionBrowserFragmentToQuickSettingsSheetDialogFragment(
sessionId = tab.id,
url = tab.content.url,
title = tab.content.title,
isSecured = tab.content.securityInfo.secure,
sitePermissions = sitePermissions,
gravity = getAppropriateLayoutGravity(),
certificateName = tab.content.securityInfo.issuer,
permissionHighlights = tab.content.permissionHighlights,
isTrackingProtectionEnabled = isTrackingProtectionEnabled,
cookieBannerUIMode = cookieBannerUIMode,
)
}
} else {
false
nav(R.id.browserFragment, directions)
}
runIfFragmentIsAttached {
val isTrackingProtectionEnabled =
tab.trackingProtection.enabled && !hasTrackingProtectionException
val directions =
BrowserFragmentDirections.actionBrowserFragmentToQuickSettingsSheetDialogFragment(
sessionId = tab.id,
url = tab.content.url,
title = tab.content.title,
isSecured = tab.content.securityInfo.secure,
sitePermissions = sitePermissions,
gravity = getAppropriateLayoutGravity(),
certificateName = tab.content.securityInfo.issuer,
permissionHighlights = tab.content.permissionHighlights,
isTrackingProtectionEnabled = isTrackingProtectionEnabled,
isCookieHandlingEnabled = !hasCookieBannerException,
)
nav(R.id.browserFragment, directions)
}
}
}

@ -8,8 +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.support.utils.DefaultSnackbarDelegate
import mozilla.components.support.utils.SnackbarDelegate
import mozilla.components.ui.widgets.DefaultSnackbarDelegate
import mozilla.components.ui.widgets.SnackbarDelegate
class CustomTabContextMenuCandidate {
companion object {

@ -6,7 +6,7 @@ package org.mozilla.fenix.browser
import android.view.View
import androidx.annotation.StringRes
import mozilla.components.support.utils.SnackbarDelegate
import mozilla.components.ui.widgets.SnackbarDelegate
import org.mozilla.fenix.components.FenixSnackbar
class FenixSnackbarDelegate(private val view: View) : SnackbarDelegate {

@ -15,9 +15,11 @@ import mozilla.components.lib.crash.service.CrashReporterService
import mozilla.components.lib.crash.service.GleanCrashReporterService
import mozilla.components.lib.crash.service.MozillaSocorroService
import mozilla.components.service.nimbus.NimbusApi
import mozilla.components.service.nimbus.messaging.FxNimbusMessaging
import mozilla.components.service.nimbus.messaging.NimbusMessagingStorage
import mozilla.components.service.nimbus.messaging.OnDiskMessageMetadataStorage
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.Config
import org.mozilla.fenix.GleanMetrics.Messaging
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.ReleaseChannel
@ -27,11 +29,9 @@ import org.mozilla.fenix.components.metrics.GleanMetricsService
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.metrics.MetricsStorage
import org.mozilla.fenix.experiments.createNimbus
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.gleanplumb.CustomAttributeProvider
import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage
import org.mozilla.fenix.gleanplumb.OnDiskMessageMetadataStorage
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.messaging.CustomAttributeProvider
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.utils.BrowsersCache
import org.mozilla.geckoview.BuildConfig.MOZ_APP_BUILDID
@ -115,6 +115,7 @@ class Analytics(
),
enabled = true,
nonFatalCrashIntent = pendingIntent,
notificationsDelegate = context.components.notificationsDelegate,
)
}
@ -147,10 +148,7 @@ class Analytics(
context = context,
metadataStorage = OnDiskMessageMetadataStorage(context),
gleanPlumb = experiments,
reportMalformedMessage = {
Messaging.malformed.record(Messaging.MalformedExtra(it))
},
messagingFeature = FxNimbus.features.messaging,
messagingFeature = FxNimbusMessaging.features.messaging,
attributeProvider = CustomAttributeProvider,
)
}

@ -10,11 +10,13 @@ import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import io.github.forkmaintainers.iceraven.components.PagedAddonCollectionProvider
import androidx.core.app.NotificationManagerCompat
import mozilla.components.feature.addons.AddonManager
import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker
import mozilla.components.feature.addons.update.DefaultAddonUpdater
import mozilla.components.feature.autofill.AutofillConfiguration
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.support.base.android.NotificationsDelegate
import mozilla.components.support.base.worker.Frequency
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.Config
@ -29,10 +31,10 @@ import org.mozilla.fenix.ext.asRecentTabs
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.filterState
import org.mozilla.fenix.ext.sort
import org.mozilla.fenix.gleanplumb.state.MessagingMiddleware
import org.mozilla.fenix.home.PocketUpdatesMiddleware
import org.mozilla.fenix.home.blocklist.BlocklistHandler
import org.mozilla.fenix.home.blocklist.BlocklistMiddleware
import org.mozilla.fenix.messaging.state.MessagingMiddleware
import org.mozilla.fenix.perf.AppStartReasonProvider
import org.mozilla.fenix.perf.StartupActivityLog
import org.mozilla.fenix.perf.StartupStateProvider
@ -85,6 +87,14 @@ class Components(private val context: Context) {
)
}
private val notificationManagerCompat = NotificationManagerCompat.from(context)
val notificationsDelegate: NotificationsDelegate by lazyMonitored {
NotificationsDelegate(
notificationManagerCompat,
)
}
val intentProcessors by lazyMonitored {
IntentProcessors(
context,
@ -113,7 +123,7 @@ class Components(private val context: Context) {
@Suppress("MagicNumber")
val addonUpdater by lazyMonitored {
DefaultAddonUpdater(context, Frequency(12, TimeUnit.HOURS))
DefaultAddonUpdater(context, Frequency(12, TimeUnit.HOURS), notificationsDelegate)
}
@Suppress("MagicNumber")

@ -241,7 +241,7 @@ class Core(
RecentlyClosedMiddleware(recentlyClosedTabsStorage, RECENTLY_CLOSED_MAX),
DownloadMiddleware(context, DownloadService::class.java),
ReaderViewMiddleware(),
TelemetryMiddleware(context.settings(), metrics, crashReporter),
TelemetryMiddleware(context, context.settings(), metrics, crashReporter),
ThumbnailsMiddleware(thumbnailStorage),
UndoMiddleware(context.getUndoDelay()),
RegionMiddleware(context, locationService),
@ -250,7 +250,7 @@ class Core(
additionalBundledSearchEngineIds = listOf("reddit", "youtube"),
migration = SearchMigration(context),
),
RecordingDevicesMiddleware(context),
RecordingDevicesMiddleware(context, context.components.notificationsDelegate),
PromptMiddleware(),
AdsTelemetryMiddleware(adsTelemetry),
LastMediaAccessMiddleware(),
@ -297,6 +297,7 @@ class Core(
R.drawable.ic_status_logo,
permissionStorage.permissionsStorage,
IntentReceiverActivity::class.java,
notificationsDelegate = context.components.notificationsDelegate,
)
MediaSessionFeature(context, MediaSessionService::class.java, this).start()

@ -8,11 +8,11 @@ import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite
import mozilla.components.lib.crash.Crash.NativeCodeCrash
import mozilla.components.lib.state.Action
import mozilla.components.service.nimbus.messaging.Message
import mozilla.components.service.nimbus.messaging.MessageSurfaceId
import mozilla.components.service.pocket.PocketStory
import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.gleanplumb.Message
import org.mozilla.fenix.gleanplumb.MessagingState
import org.mozilla.fenix.home.Mode
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory
@ -22,7 +22,7 @@ import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTabState
import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
import org.mozilla.fenix.library.history.PendingDeletionHistory
import org.mozilla.fenix.nimbus.MessageSurfaceId
import org.mozilla.fenix.messaging.MessagingState
import org.mozilla.fenix.wallpapers.Wallpaper
/**
@ -194,7 +194,18 @@ sealed class AppAction : Action {
}
/**
* Indicates that the app has been resumed and metrics that relate to that should be sent.
* [AppAction] implementations related to the application lifecycle.
*/
object ResumedMetricsAction : AppAction()
sealed class AppLifecycleAction : AppAction() {
/**
* The application has received an ON_RESUME event.
*/
object ResumeAction : AppLifecycleAction()
/**
* The application has received an ON_PAUSE event.
*/
object PauseAction : AppLifecycleAction()
}
}

@ -12,7 +12,6 @@ import mozilla.components.lib.state.State
import mozilla.components.service.pocket.PocketStory
import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
import org.mozilla.fenix.gleanplumb.MessagingState
import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.home.Mode
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
@ -22,6 +21,7 @@ import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTabState
import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
import org.mozilla.fenix.library.history.PendingDeletionHistory
import org.mozilla.fenix.messaging.MessagingState
import org.mozilla.fenix.wallpapers.WallpaperState
/**
@ -51,6 +51,7 @@ import org.mozilla.fenix.wallpapers.WallpaperState
* Also serves as an in memory cache of all stories mapped by category allowing for quick stories filtering.
*/
data class AppState(
val isForeground: Boolean = true,
val inactiveTabsExpanded: Boolean = false,
val firstFrameDrawn: Boolean = false,
val nonFatalCrashes: List<NativeCodeCrash> = emptyList(),

@ -11,11 +11,11 @@ import mozilla.components.service.pocket.ext.recordNewImpression
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.ext.filterOutTab
import org.mozilla.fenix.ext.getFilteredStories
import org.mozilla.fenix.gleanplumb.state.MessagingReducer
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory
import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTabState
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup
import org.mozilla.fenix.messaging.state.MessagingReducer
/**
* Reducer for [AppStore].
@ -130,7 +130,8 @@ internal object AppStoreReducer {
)
}
is AppAction.PocketStoriesCategoriesChange -> {
val updatedCategoriesState = state.copy(pocketStoriesCategories = action.storiesCategories)
val updatedCategoriesState =
state.copy(pocketStoriesCategories = action.storiesCategories)
// Whenever categories change stories to be displayed needs to also be changed.
updatedCategoriesState.copy(
pocketStories = updatedCategoriesState.getFilteredStories(),
@ -220,7 +221,12 @@ internal object AppStoreReducer {
val wallpaperState = state.wallpaperState.copy(availableWallpapers = wallpapers)
state.copy(wallpaperState = wallpaperState)
}
is AppAction.ResumedMetricsAction -> state
is AppAction.AppLifecycleAction.ResumeAction -> {
state.copy(isForeground = true)
}
is AppAction.AppLifecycleAction.PauseAction -> {
state.copy(isForeground = false)
}
}
}

@ -13,6 +13,7 @@ import com.adjust.sdk.AdjustConfig
import com.adjust.sdk.LogLevel
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.Config
import org.mozilla.fenix.GleanMetrics.FirstSession
import org.mozilla.fenix.ext.settings
class AdjustMetricsService(private val application: Application) : MetricsService {
@ -38,7 +39,9 @@ class AdjustMetricsService(private val application: Application) : MetricsServic
val installationPing = FirstSessionPing(application)
val timerId = FirstSession.adjustAttributionTime.start()
config.setOnAttributionChangedListener {
FirstSession.adjustAttributionTime.stopAndAccumulate(timerId)
if (!it.network.isNullOrEmpty()) {
application.applicationContext.settings().adjustNetwork =
it.network

@ -25,7 +25,7 @@ class MetricsMiddleware(
}
private fun handleAction(action: AppAction) = when (action) {
is AppAction.ResumedMetricsAction -> {
is AppAction.AppLifecycleAction.ResumeAction -> {
metrics.track(Event.GrowthData.SetAsDefault)
metrics.track(Event.GrowthData.FirstAppOpenForDay)
metrics.track(Event.GrowthData.FirstWeekSeriesActivity)

@ -148,6 +148,7 @@ class BrowserToolbarView(
store = components.core.store,
sessionId = customTabSession?.id,
shouldReverseItems = settings.toolbarPosition == ToolbarPosition.TOP,
isSandboxCustomTab = false,
onItemTapped = {
it.performHapticIfNeeded(view)
interactor.onBrowserToolbarMenuItemTapped(it)

@ -0,0 +1,100 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.compose
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Popup action dropdown menu.
*
* @param menuItems List of items to be displayed in the menu.
* @param showMenu Whether or not the menu is currently displayed to the user.
* @param onDismissRequest Invoked when user dismisses the menu or on orientation changes.
* @param modifier Modifier to be applied to the menu.
*/
@Composable
fun DropdownMenu(
menuItems: List<MenuItem>,
showMenu: Boolean,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
) {
DisposableEffect(LocalConfiguration.current.orientation) {
onDispose { onDismissRequest() }
}
MaterialTheme(shapes = MaterialTheme.shapes.copy(medium = RoundedCornerShape(2.dp))) {
DropdownMenu(
expanded = showMenu && menuItems.isNotEmpty(),
onDismissRequest = { onDismissRequest() },
modifier = Modifier
.background(color = FirefoxTheme.colors.layer2)
.then(modifier),
) {
for (item in menuItems) {
DropdownMenuItem(
onClick = {
onDismissRequest()
item.onClick()
},
) {
Text(
text = item.title,
color = item.color ?: FirefoxTheme.colors.textPrimary,
maxLines = 1,
style = FirefoxTheme.typography.subtitle1,
modifier = Modifier
.fillMaxHeight()
.align(Alignment.CenterVertically),
)
}
}
}
}
}
/**
* Represents a text item from the dropdown menu.
*
* @property title Text the item should display.
* @property color Color used to display the text.
* @property onClick Callback to be called when the item is clicked.
*/
data class MenuItem(
val title: String,
val color: Color? = null,
val onClick: () -> Unit,
)
@LightDarkPreview
@Composable
private fun DropdownMenuPreview() {
FirefoxTheme {
DropdownMenu(
listOf(
MenuItem("Rename") {},
MenuItem("Share") {},
MenuItem("Remove", FirefoxTheme.colors.textWarning) {},
),
true,
{},
)
}
}

@ -0,0 +1,128 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.compose
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.PagerState
import com.google.accompanist.pager.rememberPagerState
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.theme.FirefoxTheme
/**
* An horizontally laid out indicator for a [HorizontalPager] with the ability to leave the trail of
* indicators to show progress, instead of just showing the current one as active.
*
* @param pagerState The state object of your [HorizontalPager] to be used to observe the list's state.
* @param modifier The modifier to apply to this layout.
* @param pageCount The size of indicators should be displayed, defaults to [PagerState.pageCount].
* If you are implementing a looping pager with a much larger [PagerState.pageCount]
* than indicators should displayed, e.g. [Int.MAX_VALUE], specify you real size in this param.
* @param activeColor The color of the active page indicator, and the color of previous page
* indicators in case [leaveTrail] is set to true.
* @param inactiveColor The color of page indicators that are inactive.
* @param leaveTrail Whether to leave the trail of indicators to show progress.
* This defaults to false and just shows the current one as active.
*/
@Composable
fun PagerIndicator(
pagerState: PagerState,
modifier: Modifier = Modifier,
pageCount: Int = pagerState.pageCount,
activeColor: Color,
inactiveColor: Color,
leaveTrail: Boolean = false,
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
val showActiveModifier: (pageIndex: Int) -> Boolean =
if (leaveTrail) {
{ it <= pagerState.currentPage }
} else {
{ it == pagerState.currentPage }
}
repeat(pageCount) {
Box(
modifier = Modifier
.size(6.dp)
.background(
shape = CircleShape,
color = if (showActiveModifier(it)) {
activeColor
} else {
inactiveColor
},
),
)
}
}
}
@LightDarkPreview
@Composable
private fun PagerIndicatorPreview() {
FirefoxTheme {
Column(
modifier = Modifier
.background(FirefoxTheme.colors.layer1)
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = "Without trail",
style = FirefoxTheme.typography.caption,
color = FirefoxTheme.colors.textPrimary,
)
Spacer(modifier = Modifier.height(8.dp))
PagerIndicator(
pagerState = rememberPagerState(1),
pageCount = 3,
activeColor = FirefoxTheme.colors.actionPrimary,
inactiveColor = FirefoxTheme.colors.actionSecondary,
modifier = Modifier.align(Alignment.CenterHorizontally),
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "With trail",
style = FirefoxTheme.typography.caption,
color = FirefoxTheme.colors.textPrimary,
)
Spacer(modifier = Modifier.height(8.dp))
PagerIndicator(
pagerState = rememberPagerState(1),
pageCount = 3,
activeColor = FirefoxTheme.colors.actionPrimary,
inactiveColor = FirefoxTheme.colors.actionSecondary,
leaveTrail = true,
modifier = Modifier.align(Alignment.CenterHorizontally),
)
}
}
}

@ -0,0 +1,96 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.compose.button
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Floating action button.
*
* @param icon [Painter] icon to be displayed inside the action button.
* @param modifier [Modifier] to be applied to the action button.
* @param label Text to be displayed next to the icon.
* @param onClick Invoked when the button is clicked.
*/
@Composable
fun FloatingActionButton(
icon: Painter,
modifier: Modifier = Modifier,
contentDescription: String? = null,
label: String? = null,
onClick: () -> Unit,
) {
FloatingActionButton(
onClick = onClick,
modifier = Modifier.testTag("button.fab").then(modifier),
backgroundColor = FirefoxTheme.colors.actionPrimary,
contentColor = FirefoxTheme.colors.textActionPrimary,
) {
Row(
modifier = Modifier
.wrapContentSize()
.padding(16.dp)
.animateContentSize(),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = icon,
contentDescription = contentDescription,
tint = FirefoxTheme.colors.iconOnColor,
)
if (!label.isNullOrBlank()) {
Spacer(Modifier.width(12.dp))
Text(
text = label,
style = FirefoxTheme.typography.button,
maxLines = 1,
)
}
}
}
}
@LightDarkPreview
@Composable
private fun FloatingActionButtonPreview() {
var label by remember { mutableStateOf<String?>("LABEL") }
FirefoxTheme {
Box(Modifier.wrapContentSize()) {
FloatingActionButton(
label = label,
icon = painterResource(R.drawable.ic_new),
onClick = {
label = if (label == null) "LABEL" else null
},
)
}
}
}

@ -4,7 +4,14 @@
package org.mozilla.fenix.compose.ext
import android.os.SystemClock
import androidx.compose.foundation.clickable
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.composed
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.graphics.Color
@ -49,3 +56,29 @@ fun Modifier.dashedBorder(
)
},
)
/**
* Used when clickable needs to be debounced to prevent rapid successive clicks
* from calling the onClick function.
*
* @param debounceInterval The length of time to wait between click events in milliseconds
* @param onClick Callback for when item this modifier effects is clicked
*/
fun Modifier.debouncedClickable(
debounceInterval: Long = 1000L,
onClick: () -> Unit,
) = composed {
var lastClickTime: Long by remember { mutableStateOf(0) }
this.then(
Modifier.clickable(
onClick = {
val currentSystemTime = SystemClock.elapsedRealtime()
if (currentSystemTime - lastClickTime > debounceInterval) {
onClick()
lastClickTime = currentSystemTime
}
},
),
)
}

@ -8,8 +8,8 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -17,7 +17,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@ -104,18 +103,20 @@ private fun HomeSectionHeaderContent(
)
onShowAllClick?.let {
ClickableText(
text = AnnotatedString(text = stringResource(id = R.string.recent_tabs_show_all)),
modifier = Modifier.padding(start = 16.dp)
.semantics {
contentDescription = description
},
style = TextStyle(
color = showAllTextColor,
fontSize = 14.sp,
),
onClick = { onShowAllClick() },
)
TextButton(onClick = { onShowAllClick() }) {
Text(
text = stringResource(id = R.string.recent_tabs_show_all),
modifier = Modifier
.padding(start = 16.dp)
.semantics {
contentDescription = description
},
style = TextStyle(
color = showAllTextColor,
fontSize = 14.sp,
),
)
}
}
}
}

@ -42,12 +42,14 @@ import androidx.compose.ui.unit.sp
import androidx.core.text.BidiFormatter
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createTab
import mozilla.components.support.ktx.kotlin.MAX_URI_LENGTH
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.Divider
import org.mozilla.fenix.compose.Favicon
import org.mozilla.fenix.compose.HorizontalFadingEdgeBox
import org.mozilla.fenix.compose.ThumbnailCard
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.tabstray.ext.toDisplayTitle
import org.mozilla.fenix.theme.FirefoxTheme
/**
@ -135,7 +137,7 @@ fun TabGridItem(
isContentRtl = BidiFormatter.getInstance().isRtl(tab.content.title),
) {
Text(
text = tab.content.title,
text = tab.toDisplayTitle().take(MAX_URI_LENGTH),
fontSize = 14.sp,
maxLines = 1,
softWrap = false,

@ -28,10 +28,12 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createTab
import mozilla.components.support.ktx.kotlin.MAX_URI_LENGTH
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.ThumbnailCard
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.tabstray.ext.toDisplayTitle
import org.mozilla.fenix.theme.FirefoxTheme
/**
@ -91,7 +93,7 @@ fun TabListItem(
.weight(weight = 1f),
) {
Text(
text = tab.content.title,
text = tab.toDisplayTitle().take(MAX_URI_LENGTH),
fontSize = 16.sp,
maxLines = 2,
color = FirefoxTheme.colors.textPrimary,

@ -34,6 +34,7 @@ import java.util.Locale
* @param store reference to the application's [BrowserStore].
* @param sessionId ID of the open custom tab session.
* @param shouldReverseItems If true, reverse the menu items.
* @param isSandboxCustomTab If true, menu should not show the "Open in Firefox" and "POWERED BY FIREFOX" items.
* @param onItemTapped Called when a menu item is tapped.
*/
class CustomTabToolbarMenu(
@ -41,6 +42,7 @@ class CustomTabToolbarMenu(
private val store: BrowserStore,
private val sessionId: String?,
private val shouldReverseItems: Boolean,
private val isSandboxCustomTab: Boolean,
private val onItemTapped: (ToolbarMenu.Item) -> Unit = {},
) : ToolbarMenu {
@ -116,12 +118,12 @@ class CustomTabToolbarMenu(
private val menuItems by lazy {
val menuItems = listOf(
poweredBy,
BrowserMenuDivider(),
poweredBy.apply { visible = { !isSandboxCustomTab } },
BrowserMenuDivider().apply { visible = { !isSandboxCustomTab } },
desktopMode,
findInPage,
openInApp.apply { visible = ::shouldShowOpenInApp },
openInFenix,
openInFenix.apply { visible = { !isSandboxCustomTab } },
BrowserMenuDivider(),
menuToolbar,
)

@ -25,6 +25,7 @@ class CustomTabsIntegration(
activity: Activity,
onItemTapped: (ToolbarMenu.Item) -> Unit = {},
shouldReverseItems: Boolean,
isSandboxCustomTab: Boolean,
isPrivate: Boolean,
) : LifecycleAwareFeature, UserInteractionHandler {
@ -50,6 +51,7 @@ class CustomTabsIntegration(
store,
sessionId,
shouldReverseItems,
isSandboxCustomTab,
onItemTapped = onItemTapped,
)
}

@ -20,6 +20,8 @@ import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.ext.components
import java.security.InvalidParameterException
const val EXTRA_IS_SANDBOX_CUSTOM_TAB = "org.mozilla.fenix.customtabs.EXTRA_IS_SANDBOX_CUSTOM_TAB"
/**
* Activity that holds the [ExternalAppBrowserFragment] that is launched within an external app,
* such as custom tabs and progressive web apps.
@ -77,6 +79,7 @@ open class ExternalAppBrowserActivity : HomeActivity() {
NavGraphDirections.actionGlobalExternalAppBrowser(
activeSessionId = customTabSessionId,
webAppManifest = manifest,
isSandboxCustomTab = intent.getBooleanExtra(EXTRA_IS_SANDBOX_CUSTOM_TAB, false),
)
else -> throw InvalidParameterException(
"Tried to navigate to ExternalAppBrowserFragment from $from",

@ -33,12 +33,12 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BaseBrowserFragment
import org.mozilla.fenix.browser.CustomTabContextMenuCandidate
import org.mozilla.fenix.browser.FenixSnackbarDelegate
import org.mozilla.fenix.components.components
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.settings.quicksettings.protections.cookiebanners.getCookieBannerUIMode
/**
* Fragment used for browsing the web within external apps.
@ -60,7 +60,8 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
val components = activity.components
val toolbar = binding.root.findViewById<BrowserToolbar>(R.id.toolbar)
val manifest = args.webAppManifest?.let { json -> WebAppManifestParser().parse(json).getOrNull() }
val manifest =
args.webAppManifest?.let { json -> WebAppManifestParser().parse(json).getOrNull() }
customTabsIntegration.set(
feature = CustomTabsIntegration(
@ -72,6 +73,7 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
onItemTapped = { browserToolbarInteractor.onBrowserToolbarMenuItemTapped(it) },
isPrivate = tab.content.private,
shouldReverseItems = !activity.settings().shouldUseBottomToolbar,
isSandboxCustomTab = args.isSandboxCustomTab,
),
owner = this,
view = view,
@ -146,6 +148,7 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
customTabSessionId,
manifest,
),
notificationsDelegate = requireComponents.notificationsDelegate,
),
)
} else {
@ -154,6 +157,7 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
activity.applicationContext,
requireComponents.core.store,
customTabSessionId,
requireComponents.notificationsDelegate,
),
)
}
@ -164,14 +168,13 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
}
override fun navToQuickSettingsSheet(tab: SessionState, sitePermissions: SitePermissions?) {
val cookieBannersStorage = requireComponents.core.cookieBannersStorage
requireComponents.useCases.trackingProtectionUseCases.containsException(tab.id) { contains ->
lifecycleScope.launch(Dispatchers.IO) {
val hasException = if (requireContext().settings().shouldUseCookieBanner) {
cookieBannersStorage.hasException(tab.content.url, tab.content.private)
} else {
false
}
lifecycleScope.launch {
val cookieBannersStorage = requireComponents.core.cookieBannersStorage
val cookieBannerUIMode = cookieBannersStorage.getCookieBannerUIMode(
requireContext(),
tab,
)
withContext(Dispatchers.Main) {
runIfFragmentIsAttached {
val directions = ExternalAppBrowserFragmentDirections
@ -185,7 +188,7 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
certificateName = tab.content.securityInfo.issuer,
permissionHighlights = tab.content.permissionHighlights,
isTrackingProtectionEnabled = tab.trackingProtection.enabled && !contains,
isCookieHandlingEnabled = !hasException,
cookieBannerUIMode = cookieBannerUIMode,
)
nav(R.id.externalAppBrowserFragment, directions)
}

@ -20,8 +20,9 @@ import androidx.lifecycle.LifecycleOwner
import mozilla.components.browser.state.selector.findCustomTab
import mozilla.components.browser.state.state.ExternalAppType
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.support.base.android.NotificationsDelegate
import mozilla.components.support.base.ids.SharedIdsHelper
import mozilla.components.support.base.ids.cancel
import mozilla.components.support.base.ids.notify
import org.mozilla.fenix.R
/**
@ -31,12 +32,16 @@ class PoweredByNotification(
private val applicationContext: Context,
private val store: BrowserStore,
private val customTabId: String,
private val notificationsDelegate: NotificationsDelegate,
) : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
if (store.state.findCustomTab(customTabId)?.config?.externalAppType === ExternalAppType.TRUSTED_WEB_ACTIVITY) {
NotificationManagerCompat.from(applicationContext)
.notify(applicationContext, NOTIFICATION_TAG, buildNotification())
notificationsDelegate.notify(
NOTIFICATION_TAG,
SharedIdsHelper.getIdForTag(applicationContext, NOTIFICATION_TAG),
buildNotification(),
)
}
}

@ -6,6 +6,7 @@ package org.mozilla.fenix.downloads
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.downloads.AbstractFetchDownloadService
import mozilla.components.support.base.android.NotificationsDelegate
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
@ -13,4 +14,5 @@ class DownloadService : AbstractFetchDownloadService() {
override val httpClient by lazy { components.core.client }
override val store: BrowserStore by lazy { components.core.store }
override val style: Style by lazy { Style(R.color.fx_mobile_text_color_accent) }
override val notificationsDelegate: NotificationsDelegate by lazy { components.notificationsDelegate }
}

@ -8,6 +8,9 @@ import android.content.Context
import mozilla.components.service.nimbus.NimbusApi
import mozilla.components.service.nimbus.NimbusAppInfo
import mozilla.components.service.nimbus.NimbusBuilder
import mozilla.components.service.nimbus.loggingErrorReporter
import mozilla.components.service.nimbus.messaging.FxNimbusMessaging
import mozilla.components.service.nimbus.messaging.NimbusSystem
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.experiments.nimbus.NimbusInterface
import org.mozilla.experiments.nimbus.internal.NimbusException
@ -15,9 +18,8 @@ import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.gleanplumb.CustomAttributeProvider
import org.mozilla.fenix.messaging.CustomAttributeProvider
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.nimbus.NimbusSystem
import org.mozilla.fenix.utils.Settings
/**
@ -58,9 +60,16 @@ fun createNimbus(context: Context, urlString: String?): NimbusApi {
return NimbusBuilder(context).apply {
url = urlString
errorReporter = { message, e ->
Logger.error("Nimbus error: $message", e)
if (BuildConfig.BUILD_TYPE == "debug") {
Logger.error("Nimbus error: $message", e)
}
if (e !is NimbusException || e.isReportableError()) {
context.components.analytics.crashReporter.submitCaughtException(e)
@Suppress("TooGenericExceptionCaught")
try {
context.components.analytics.crashReporter.submitCaughtException(e)
} catch (e: Throwable) {
loggingErrorReporter(message, e)
}
}
}
initialExperiments = R.raw.initial_experiments
@ -97,20 +106,20 @@ fun NimbusException.isReportableError(): Boolean {
*/
fun NimbusInterface.maybeFetchExperiments(
context: Context,
feature: NimbusSystem = FxNimbus.features.nimbusSystem.value(),
feature: NimbusSystem = FxNimbusMessaging.features.nimbusSystem.value(),
currentTimeMillis: Long = System.currentTimeMillis(),
) {
val minimumPeriodMinutes = if (!context.settings().nimbusUsePreview) {
feature.refreshIntervalForeground
if (context.settings().nimbusUsePreview) {
context.settings().nimbusLastFetchTime = 0L
fetchExperiments()
} else {
0
}
val minimumPeriodMinutes = feature.refreshIntervalForeground
val lastFetchTimeMillis = context.settings().nimbusLastFetchTime
val minimumPeriodMillis = minimumPeriodMinutes * Settings.ONE_MINUTE_MS
val lastFetchTimeMillis = context.settings().nimbusLastFetchTime
val minimumPeriodMillis = minimumPeriodMinutes * Settings.ONE_MINUTE_MS
if (currentTimeMillis - lastFetchTimeMillis >= minimumPeriodMillis) {
context.settings().nimbusLastFetchTime = currentTimeMillis
fetchExperiments()
if (currentTimeMillis - lastFetchTimeMillis >= minimumPeriodMillis) {
context.settings().nimbusLastFetchTime = currentTimeMillis
fetchExperiments()
}
}
}

@ -0,0 +1,116 @@
/* 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.experiments
import android.annotation.SuppressLint
import android.content.pm.ActivityInfo
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.DialogFragment
import androidx.navigation.fragment.navArgs
import org.mozilla.fenix.R
import org.mozilla.fenix.experiments.view.ResearchSurfaceSurvey
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Dialog displaying the fullscreen research surface message.
*/
class ResearchSurfaceDialogFragment : DialogFragment() {
private val args by navArgs<ResearchSurfaceDialogFragmentArgs>()
private lateinit var bundleArgs: Bundle
/**
* A callback to trigger the 'Take Survey' button of the dialog.
*/
var onAccept: () -> Unit = {}
/**
* A callback to trigger the 'No Thanks' button of the dialog.
*/
var onDismiss: () -> Unit = {}
@SuppressLint("SourceLockedOrientationActivity")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, R.style.ResearchSurfaceDialogStyle)
bundleArgs = args.toBundle()
}
override fun onDestroy() {
super.onDestroy()
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View = ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
val messageText = bundleArgs.getString(KEY_MESSAGE_TEXT)
?: getString(R.string.nimbus_survey_message_text)
val acceptButtonText = bundleArgs.getString(KEY_ACCEPT_BUTTON_TEXT)
?: getString(R.string.preferences_take_survey)
val dismissButtonText = bundleArgs.getString(KEY_DISMISS_BUTTON_TEXT)
?: getString(R.string.preferences_not_take_survey)
setContent {
FirefoxTheme {
ResearchSurfaceSurvey(
messageText = messageText,
onAcceptButtonText = acceptButtonText,
onDismissButtonText = dismissButtonText,
onDismiss = {
onDismiss()
dismiss()
},
onAccept = {
onAccept()
dismiss()
},
)
}
}
}
companion object {
/**
* A builder method for creating a [ResearchSurfaceDialogFragment]
*/
fun newInstance(
keyMessageText: String?,
keyAcceptButtonText: String?,
keyDismissButtonText: String?,
): ResearchSurfaceDialogFragment {
val fragment = ResearchSurfaceDialogFragment()
val arguments = fragment.arguments ?: Bundle()
with(arguments) {
putString(KEY_MESSAGE_TEXT, keyMessageText)
putString(KEY_ACCEPT_BUTTON_TEXT, keyAcceptButtonText)
putString(KEY_DISMISS_BUTTON_TEXT, keyDismissButtonText)
}
fragment.arguments = arguments
fragment.isCancelable = false
return fragment
}
private const val KEY_MESSAGE_TEXT = "KEY_MESSAGE_TEXT"
private const val KEY_ACCEPT_BUTTON_TEXT = "KEY_ACCEPT_BUTTON_TEXT"
private const val KEY_DISMISS_BUTTON_TEXT = "KEY_DISMISS_BUTTON_TEXT"
const val FRAGMENT_TAG = "MOZAC_RESEARCH_SURFACE_DIALOG_FRAGMENT"
}
}

@ -0,0 +1,165 @@
/* 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.experiments.view
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
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.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.compose.button.PrimaryButton
import org.mozilla.fenix.compose.button.SecondaryButton
import org.mozilla.fenix.theme.FirefoxTheme
/**
* The ratio of the content height to screen height. This was determined from the designs in figma
* taking the top and bottom padding to be 10% of screen height.
*/
private const val FULLSCREEN_HEIGHT = 0.8f
/**
* The ratio of the button width to screen width. This was determined from the designs in figma
* taking the horizontal button paddings to be 5% of the screen width.
*/
private const val BUTTON_WIDTH = 0.9f
/**
* Values used in slide in animation.
* These values were confirmed through demo builds and UX review.
*/
private const val INITIAL_OFFSET = 1000
private const val ANIMATION_DURATION_MS = 500
@Composable
private fun SlideInFromBottomAnimation(
content: @Composable () -> Unit,
) {
var offsetY by remember { mutableStateOf(INITIAL_OFFSET) }
val offsetState by animateDpAsState(
targetValue = offsetY.dp,
animationSpec = tween(durationMillis = ANIMATION_DURATION_MS),
)
Box(
modifier = Modifier
.fillMaxSize()
.offset(y = offsetState),
) {
content()
}
LaunchedEffect(Unit) {
offsetY = 0
}
}
/**
* A full screen for displaying a research surface.
*
* @param messageText The research surface message text to be displayed.
* @param onAcceptButtonText A positive button text of the fullscreen message.
* @param onDismissButtonText A negative button text of the fullscreen message.
* @param onDismiss Invoked when the user clicks on the "No Thanks" button.
* @param onAccept Invoked when the user clicks on the "Take Survey" button
*/
@Composable
fun ResearchSurfaceSurvey(
messageText: String,
onAcceptButtonText: String,
onDismissButtonText: String,
onDismiss: () -> Unit,
onAccept: () -> Unit,
) {
SlideInFromBottomAnimation {
Box(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding()
.navigationBarsPadding(),
) {
Column(
modifier = Modifier
.fillMaxHeight(FULLSCREEN_HEIGHT)
.align(Alignment.Center)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween,
) {
Spacer(Modifier)
Column(
modifier = Modifier.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painter = painterResource(R.drawable.ic_firefox),
contentDescription = null,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = messageText,
color = FirefoxTheme.colors.textPrimary,
textAlign = TextAlign.Center,
style = FirefoxTheme.typography.headline6,
)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth(BUTTON_WIDTH),
) {
PrimaryButton(
text = onAcceptButtonText,
onClick = onAccept,
)
Spacer(modifier = Modifier.height(8.dp))
SecondaryButton(
text = onDismissButtonText,
onClick = onDismiss,
)
}
}
}
}
}
@Composable
@LightDarkPreview
private fun SurveyPreview() {
FirefoxTheme {
ResearchSurfaceSurvey(
messageText = stringResource(id = R.string.nimbus_survey_message_text),
onAcceptButtonText = stringResource(id = R.string.preferences_take_survey),
onDismissButtonText = stringResource(id = R.string.preferences_not_take_survey),
onDismiss = {},
onAccept = {},
)
}
}

@ -69,10 +69,12 @@ fun Activity.breadcrumb(
*
* @param from fallback direction in case, couldn't open the setting.
* @param flags fallback flags for when opening the Sumo article page.
* @param useCustomTab fallback to open the Sumo article in a custom tab.
*/
fun Activity.openSetDefaultBrowserOption(
from: BrowserDirection = BrowserDirection.FromSettings,
flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none(),
useCustomTab: Boolean = false,
) {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
@ -94,14 +96,24 @@ fun Activity.openSetDefaultBrowserOption(
navigateToDefaultBrowserAppsSettings()
}
else -> {
(this as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = SupportUtils.getGenericSumoURLForTopic(
topic = SupportUtils.SumoTopic.SET_AS_DEFAULT_BROWSER,
),
newTab = true,
from = from,
flags = flags,
val sumoDefaultBrowserUrl = SupportUtils.getGenericSumoURLForTopic(
topic = SupportUtils.SumoTopic.SET_AS_DEFAULT_BROWSER,
)
if (useCustomTab) {
startActivity(
SupportUtils.createSandboxCustomTabIntent(
context = this,
url = sumoDefaultBrowserUrl,
),
)
} else {
(this as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = sumoDefaultBrowserUrl,
newTab = true,
from = from,
flags = flags,
)
}
}
}
}

@ -1,62 +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.gleanplumb
import org.mozilla.fenix.nimbus.MessageData
import org.mozilla.fenix.nimbus.MessageSurfaceId
import org.mozilla.fenix.nimbus.StyleData
/**
* A data class that holds a representation of GleanPlum message from Nimbus.
*
* @param id identifies a message as unique.
* @param data Data information provided from Nimbus.
* @param action A strings that represents which action should be performed
* after a message is clicked.
* @param style Indicates how a message should be styled.
* @param triggers A list of strings corresponding to targeting expressions. The message
* will be shown if all expressions `true`.
* @param metadata Metadata that help to identify if a message should shown.
*/
data class Message(
val id: String,
val data: MessageData,
val action: String,
val style: StyleData,
val triggers: List<String>,
val metadata: Metadata,
) {
val maxDisplayCount: Int
get() = style.maxDisplayCount
val priority: Int
get() = style.priority
val surface: MessageSurfaceId
get() = data.surface
val isExpired: Boolean
get() = metadata.displayCount >= maxDisplayCount
/**
* A data class that holds metadata that help to identify if a message should shown.
*
* @param id identifies a message as unique.
* @param displayCount Indicates how many times a message is displayed.
* @param pressed Indicates if a message has been clicked.
* @param dismissed Indicates if a message has been closed.
* @param lastTimeShown A timestamp indicating when was the last time, the message was shown.
* @param latestBootIdentifier A unique boot identifier for when the message was last displayed
* (this may be a boot count or a boot id).
*/
data class Metadata(
val id: String,
val displayCount: Int = 0,
val pressed: Boolean = false,
val dismissed: Boolean = false,
val lastTimeShown: Long = 0L,
val latestBootIdentifier: String? = null,
)
}

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

Loading…
Cancel
Save