Merge pull request #393 from fork-maintainers/upstream-sync

Sync with Mozilla Firefox v92.1.1
pull/420/head iceraven-1.13.0
interfect 3 years ago committed by GitHub
commit 8b1abfbbf0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -50,4 +50,8 @@
# Possible regressions throughout the app
*.pro @mozilla-mobile/Performance
*proguard* @mozilla-mobile/Performance
# This file configures perf tests via Jetpack Benchmark.
app/benchmark.gradle @mozilla-mobile/Performance
# --- PERFORMANCE END --- #

@ -1,23 +0,0 @@
---
name: "\U0001F41E Bug report"
about: Create a report to help us improve
title: "[Bug]"
labels: "\U0001F41E bug"
assignees: ''
---
[comment]: # (Please do your best to search for duplicate issues before filing a new issue so we can keep our issue board clean)
[comment]: # (Every issue should have the exact bug and steps to reproduce described in it. Please do not file feedback list tickets as it is difficult to parse them and address their individual points)
[comment]: # (Read https://github.com/mozilla-mobile/fenix#i-want-to-file-an-issue for more information)
## Steps to reproduce
### Expected behavior
### Actual behavior
### Device information
* Device vendor / model and Android version: ?
* Firefox for Android version: ? (go to Settings -> About Firefox)

@ -0,0 +1,91 @@
name: "\U0001F41E Bug report"
description: Create a report to help us improve.
title: "[Bug]: "
labels: ["\U0001F41E bug", "needs:triage"]
body:
- type: markdown
attributes:
value: |
- Please do your best to search for duplicate issues before filing a new issue so we can keep our issue board clean.
- Have a look at ["I want to file an issue!"][info] for more information.
- Read the [Community Participation Guidelines][guidelines] and the [Bugzilla Etiquette guidelines][bugzilla] before filing an issue.
[info]: https://github.com/mozilla-mobile/fenix#i-want-to-file-an-issue
[guidelines]: https://www.mozilla.org/en-US/about/governance/policies/participation/
[bugzilla]: https://bugzilla.mozilla.org/page.cgi?id=etiquette.html
- type: textarea
attributes:
label: Steps to reproduce
description: Steps to reproduce the behaviour.
placeholder: |
1. Have a tab open..
2. Go to..
3. Click on..
4. Observe..
validations:
required: true
- type: textarea
attributes:
label: Expected behaviour
placeholder: A menu should open..
validations:
required: true
- type: textarea
attributes:
label: Actual behaviour
placeholder: The app closes unexpectedly..
validations:
required: true
- type: markdown
attributes:
value: |
# Device information
- type: input
attributes:
label: Device name
description: The name of the device model and manufacturer.
placeholder: Google Pixel 2
validations:
required: false
- type: input
attributes:
label: Android version
description: You can find the Android version information in the About section of your device's system settings.
placeholder: Android 10
validations:
required: true
- type: dropdown
attributes:
label: Firefox release type
description: You can find this information in Settings -> About Firefox.
options:
- Firefox Nightly
- Firefox Beta
- Firefox
validations:
required: true
- type: input
attributes:
label: Firefox version
description: You can find this information in Settings -> About Firefox.
placeholder: 89.0.10
validations:
required: true
- type: textarea
attributes:
label: Device logs
description: |
Device logs or crash information can greatly aid in debugging. You can find some details here on how to [retrieve device logs or crash IDs][log].
[log]: https://github.com/mozilla-mobile/fenix/wiki/Logging-Crash-Information
validations:
required: false
- type: textarea
attributes:
label: Additional information
description: |
If you have any additional information for us, use the field below.
Please note, you can attach screenshots or screen recordings here, by
dragging and dropping files in the field below.
validations:
required: false

@ -28,7 +28,7 @@ There are two releases this covers: the current sprint that is going out to Beta
## Sprint X.1 End [Wednesday, 2nd week] Cutting a Beta
- [ ] Make a new Beta
- [ ] Create a branch off of master (DO NOT PUSH YET) for the *current* milestone of format `releases_v85.0.0`. After that, anything landing in master will be part of the next release.
- [ ] Create a branch off of main (DO NOT PUSH YET) for the *current* milestone of format `releases_v85.0.0`. After that, anything landing in main will be part of the next release.
- ⚠️ Please **do not** use `/` in branch names anymore: Taskcluster silently ignores them and doesn't output tasks at the right index.
- [ ] Bump `version.txt` to match the new version number
- [ ] Grant [mozilla-release-automation-bot](https://github.com/mozilla-release-automation-bot) write access to this branch.
@ -41,7 +41,7 @@ There are two releases this covers: the current sprint that is going out to Beta
- [ ] Send an email to QA at mozilla-mobile-qa@softvision.com with a link to the Taskcluster build (subdirectory of the [Fenix CI](https://firefox-ci-tc.services.mozilla.com/tasks/index/mobile.v2.fenix.beta))
### Bugfix uplifts / Beta Product Integrity (Beta Release until PI green signoff)
- [ ] If bugs are considered release blocker then find someone to fix them on master and the milestone branch (cherry-pick / uplift)
- [ ] If bugs are considered release blocker then find someone to fix them on main and the milestone branch (cherry-pick / uplift)
- [ ] Add the uplift request to the appropriate row in the [Uplifts document](https://docs.google.com/spreadsheets/d/1qIvHpcQ3BqJtlzV5T4M1MhbWVxkNiG-ToeYnWEBW4-I/edit#gid=0).
- [ ] If needed, ship a new beta version (e.g. v1.0-beta.2) and follow the submission checklist again.
- [ ] Once there is GREEN QA signoff, file a [release management bugzilla for rollout](https://bugzilla.mozilla.org/show_bug.cgi?id=1664366)
@ -49,8 +49,8 @@ There are two releases this covers: the current sprint that is going out to Beta
### Uplifting L10N strings to Beta [Wednesday, 2 weeks after sprint end]
- [ ] Find the issue ([example](https://github.com/mozilla-mobile/fenix/issues/16381)) filed by L10N / delphine saying string are ready for uplift (it takes 2 weeks for localizers to prepare localization).
- [ ] If there are new locales that are ready to be added to Release, add them to [l10n-release.toml](https://github.com/mozilla-mobile/fenix/blob/master/l10n-release.toml)
- [ ] Run the [L10N uplift script](https://github.com/mozilla-mobile/fenix/blob/master/l10n-uplift.py) against the releases/vX.1 branch (releases/v85.0.0). There will likely be conflicts, but if you are confused, they should match the strings in [main/Nightly](https://github.com/mozilla-mobile/fenix/tree/master/app/src/main/res)
- [ ] If there are new locales that are ready to be added to Release, add them to [l10n-release.toml](https://github.com/mozilla-mobile/fenix/blob/main/l10n-release.toml)
- [ ] Run the [L10N uplift script](https://github.com/mozilla-mobile/fenix/blob/main/l10n-uplift.py) against the releases/vX.1 branch (releases/v85.0.0). There will likely be conflicts, but if you are confused, they should match the strings in [main/Nightly](https://github.com/mozilla-mobile/fenix/tree/main/app/src/main/res)
- [ ] Once all conflicts are resolved, tag a new Beta to be released.
- [ ] Notify delphine in the L10N issue that the strings have been uplifted, and string quarantine can be lifted

1
.github/stale.yml vendored

@ -14,6 +14,7 @@ onlyLabels: []
exemptLabels:
- pin
- "feature request 🌟"
- "eng:disabled-test"
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: false

@ -10,7 +10,7 @@ jobs:
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 1.8
java-version: 11
- name: "Clean & Assemble Debug"
uses: eskatos/gradle-command-action@v1
with:
@ -29,7 +29,7 @@ jobs:
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 1.8
java-version: 11
- name: "Test Debug Unit Tests"
uses: eskatos/gradle-command-action@v1
with:
@ -48,7 +48,7 @@ jobs:
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 1.8
java-version: 11
- name: "Detekt"
uses: eskatos/gradle-command-action@v1
with:
@ -72,7 +72,7 @@ jobs:
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 1.8
java-version: 11
- name: "Ktlint"
uses: eskatos/gradle-command-action@v1
with:
@ -91,7 +91,7 @@ jobs:
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 1.8
java-version: 11
- name: "Lint Debug"
uses: eskatos/gradle-command-action@v1
with:
@ -106,7 +106,7 @@ jobs:
path: app/build/reports/lint-results-debug.html
run-ui:
runs-on: macos-latest
runs-on: macos-11
if: github.event.pull_request.head.repo.full_name != github.repository && github.actor != 'MickeyMoz'
timeout-minutes: 60
@ -125,7 +125,7 @@ jobs:
arch: x86_64
profile: pixel_3a
script:
"./gradlew connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=\
"JAVA_HOME=$JAVA_HOME_11_X64 && ./gradlew connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=\
org.mozilla.fenix.ui.NavigationToolbarTest#visitURLTest"
- name: Upload Test Artifacts
uses: actions/upload-artifact@v2

@ -15,12 +15,15 @@ jobs:
steps:
- name: "Discover Fenix Beta Version"
id: fenix-beta-version
uses: mozilla-mobile/fenix-beta-version@1.1.0
uses: mozilla-mobile/fenix-beta-version@2.0.0
- name: "Skip non-beta versions"
uses: andymckay/cancel-action@0.2
if: ${{ steps.fenix-beta-version.outputs.fenix-beta-version == '' }}
- name: "Checkout Master Branch"
uses: actions/checkout@v2
with:
path: main
ref: master
ref: main
- name: "Checkout Beta Branch"
uses: actions/checkout@v2
with:
@ -37,5 +40,6 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
path: beta
branch: automation/sync-strings-${{ steps.fenix-beta-version.outputs.major-beta-version }}
title: "Sync Strings from master to releases_${{steps.fenix-beta-version.outputs.fenix-beta-version}}.0"
body: "This (automated) PR syncs strings from `master` to `releases_${{steps.fenix-beta-version.outputs.fenix-beta-version}}.0.0`"
title: "Sync Strings from main to releases_${{steps.fenix-beta-version.outputs.fenix-beta-version}}.0"
body: "This (automated) PR syncs strings from `main` to `releases_${{steps.fenix-beta-version.outputs.fenix-beta-version}}.0.0`"
labels: needs:review

@ -86,3 +86,27 @@ pull_request_rules:
strict: smart
delete_head_branch:
force: false
- name: Needs landing - Rebase
conditions:
- check-success=pr-complete
- label=pr:needs-landing
- "#approved-reviews-by>=1"
- -draft
- label!=pr:work-in-progress
- label!=pr:do-not-land
actions:
merge:
method: rebase
strict: smart
- name: Needs landing - Squash
conditions:
- check-success=pr-complete
- label=pr:needs-landing-squashed
- "#approved-reviews-by>=1"
- -draft
- label!=pr:work-in-progress
- label!=pr:do-not-land
actions:
merge:
method: squash
strict: smart

@ -8,7 +8,7 @@ tasks:
- $let:
taskgraph:
branch: taskgraph
revision: a458418ef7cdd6778f1283926c6116966255bc24
revision: 9daff451cfbe82c5c70237a7b3dbcf4fd3238299
trustDomain: mobile
in:
$let:
@ -243,7 +243,7 @@ tasks:
# Note: This task is built server side without the context or tooling that
# exist in tree so we must hard code the hash
image:
mozillareleases/taskgraph:decision-mobile-682fbaa1ef17e70ddfe3457da3eaf8e776c4a20fe5bfbdbeba0641fd5bceae2a@sha256:bbb2613aaab79d17e590fbd78c072d0643be40fd1237195703f84280ecc3b302
mozillareleases/taskgraph:decision-mobile-44b6b7b4c370220eff56efa8b508aa5157ef9c6e74847c7ecc19d640946ba49e@sha256:4107cbc5e154502529e4d38efa4dc89c05ee54e2cbc6e2e66023e68407502894
maxRunTime: 1800
@ -260,14 +260,14 @@ tasks:
in:
$if: 'tasks_for == "action"'
then: >
PIP_IGNORE_INSTALLED=0 pip install --user /builds/worker/checkouts/taskgraph &&
PIP_IGNORE_INSTALLED=0 pip install --user mozilla-version &&
PIP_IGNORE_INSTALLED=0 pip3 install --user /builds/worker/checkouts/taskgraph &&
PIP_IGNORE_INSTALLED=0 pip3 install --user mozilla-version &&
taskcluster/scripts/decision-install-sdk.sh &&
ln -s /builds/worker/artifacts artifacts &&
~/.local/bin/taskgraph action-callback
else: >
PIP_IGNORE_INSTALLED=0 pip install --user /builds/worker/checkouts/taskgraph &&
PIP_IGNORE_INSTALLED=0 pip install --user mozilla-version &&
PIP_IGNORE_INSTALLED=0 pip3 install --user /builds/worker/checkouts/taskgraph &&
PIP_IGNORE_INSTALLED=0 pip3 install --user mozilla-version &&
taskcluster/scripts/decision-install-sdk.sh &&
ln -s /builds/worker/artifacts artifacts &&
~/.local/bin/taskgraph decision

8
Jenkinsfile vendored

@ -1,7 +1,7 @@
pipeline {
agent any
triggers {
cron(env.BRANCH_NAME == 'master' ? 'H 0 * * *' : '')
cron(env.BRANCH_NAME == 'main' ? 'H 0 * * *' : '')
}
options {
timestamps()
@ -9,7 +9,7 @@ pipeline {
}
stages {
stage('test') {
when { branch 'master' }
when { branch 'main' }
steps {
dir('app/src/androidTest/java/org/mozilla/fenix/syncIntegration') {
sh 'pipenv install'
@ -22,7 +22,7 @@ pipeline {
post {
always {
script {
if (env.BRANCH_NAME == 'master') {
if (env.BRANCH_NAME == 'main') {
publishHTML(target: [
allowMissing: false,
alwaysLinkToLastBuild: true,
@ -36,7 +36,7 @@ pipeline {
failure {
script {
if (env.BRANCH_NAME == 'master') {
if (env.BRANCH_NAME == 'main') {
slackSend(
color: 'danger',
message: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL}HTML_20Report/)")

@ -0,0 +1,66 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
// This comment contains the central documentation for how we configured Jetpack Benchmark. Currently:
// - microbenchmark: configured differently than recommended (see inline notes below)
// - macrobenchmark: not configured
//
// To run our benchmarks, you need to set the "benchmark" gradle property. You can:
// - (preferred) Run via the command line (change the class you run on):
// ./gradlew -Pbenchmark app:connectedCheck -Pandroid.testInstrumentationRunnerArguments.class=org.mozilla.fenix.perf.SampleBenchmark
// - Use the IDE. Temporarily set the "benchmark" property in app/build.gradle with "ext.benchmark=true"
// near the top of the file. DO NOT COMMIT THIS.
// - (note: I was unable to get IDE run configurations working)
//
// To get the results, look at this file (we recommend using the median; results are in nanoseconds):
// app/build/outputs/connected_android_test_additional_output/nightlyAndroidTest/connected/<device>/org.mozilla.fenix-benchmarkData.json
//
// I was unable to get the results to print directly in Android Studio (perhaps it's my device).
//
// The official documentation suggests configuring microbenchmark in a separate module. This would
// require any benchmarked code to be in a library module, not the :app module (see below). To avoid
// this requirement, we created the "benchmark" gradle property.
//
// For the most accurate results, the documentation recommends running tests on rooted devices with
// the CPU clock locked.
//
// See https://developer.android.com/studio/profile/benchmark#what-to-benchmark for when writing a
// jetpack microbenchmark is a good fit.
// I think `android` represents this object:
// https://google.github.io/android-gradle-dsl/3.3/com.android.build.gradle.AppExtension.html
ext.maybeConfigForJetpackBenchmark = { android ->
if (!project.hasProperty("benchmark")) {
return
}
// The official documentation https://developer.android.com/studio/profile/benchmark#full-setup
// recommends setting up the Microbenchmark library in a separate module from your app: AFAICT,
// the reason for this is to prevent the benchmarks from being configured against debug
// builds. We chose not to do this because it's a lot of work to pull code out into a
// separate module just to benchmark it. We were able to replicate the outcome by setting
// this testBuildType property.
android.testBuildType "nightly"
// WARNING: our proguard configuration for androidTest is not set up correctly so the tests
// fail if we don't disable minification. DISABLING MINIFICATION PRODUCES BENCHMARKS THAT ARE
// LESS REPRESENTATIVE TO THE USER EXPERIENCE, however, so we made this tradeoff to reduce
// implementation time.
project.ext.disableOptimization = true
android.defaultConfig {
// WARNING: the benchmark framework warns you if you're running the test in a configuration
// that will compromise the accuracy of the results. Unfortunately, I couldn't get everything
// working so I had to suppress some things.
//
// - ACTIVITY-MISSING: we're supposed to use the test instrumentation runner,
// "androidx.benchmark.junit4.AndroidBenchmarkRunner". However, when I do so, I get an error
// that we're unable to launch the activity. My understanding is that this runner will use an
// "IsolationActivity" to reduce the impact of other work on the device from affecting the benchmark
// and to opt into a lower-max CPU frequency on unrooted devices that support it
// - UNLOCKED: ./gradlew lockClocks, which locks the CPU frequency, fails on my device. See
// https://issuetracker.google.com/issues/176836267 for potential workarounds.
testInstrumentationRunnerArgument 'androidx.benchmark.suppressErrors', 'ACTIVITY-MISSING,UNLOCKED'
}
}

@ -8,6 +8,7 @@ apply plugin: 'kotlin-android-extensions'
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
@ -17,9 +18,12 @@ import org.gradle.internal.logging.text.StyledTextOutputFactory
import static org.gradle.api.tasks.testing.TestResult.ResultType
apply from: 'benchmark.gradle'
android {
compileSdkVersion Config.compileSdkVersion
project.maybeConfigForJetpackBenchmark(it)
if (project.hasProperty("testBuildType")) {
// Allowing to configure the test build type via command line flag (./gradlew -PtestBuildType=beta ..)
// in order to run UI tests against other build variants than debug in automation.
@ -75,7 +79,7 @@ android {
// in automation for UI testing non-debug builds.
shrinkResources !project.hasProperty("disableOptimization")
minifyEnabled !project.hasProperty("disableOptimization")
proguardFiles 'proguard-android-optimize-3.5.0-modified.txt', 'proguard-rules.pro'
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
matchingFallbacks = ['release'] // Use on the "release" build type in dependencies (AARs)
if (gradle.hasProperty("localProperties.autosignReleaseWithDebugKey")) {
@ -174,6 +178,10 @@ android {
}
buildFeatures {
viewBinding true
}
aaptOptions {
// All JavaScript code used internally by GeckoView is packaged in a
// file called omni.ja. If this file is compressed in the APK,
@ -200,7 +208,6 @@ android {
}
beta {
java.srcDirs = ['src/migration/java']
manifest.srcFile "src/migration/AndroidManifest.xml"
}
release {
java.srcDirs = ['src/migration/java']
@ -253,6 +260,14 @@ android {
minHeapSize = "1024m"
}
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion = Versions.androidx_compose
}
}
android.applicationVariants.all { variant ->
@ -293,12 +308,15 @@ android.applicationVariants.all { variant ->
println("versionName override: $versionName")
variant.outputs.each { output ->
def isMozillaOnline = project.hasProperty("mozillaOnline") || gradle.hasProperty("localProperties.mozillaOnline")
def abi = output.getFilter(OutputFile.ABI)
// If it is a Mozilla Online build, use a unified version code of armeabi-v7a
def arch = (isMozillaOnline) ? "armeabi-v7a" : abi
// We use the same version code generator, that we inherited from Fennec, across all channels - even on
// channels that never shipped a Fennec build.
def versionCodeOverride = Config.generateFennecVersionCode(abi)
def versionCodeOverride = Config.generateFennecVersionCode(arch)
println("versionCode for $abi = $versionCodeOverride")
println("versionCode for $abi = $versionCodeOverride, isMozillaOnline = $isMozillaOnline")
output.versionNameOverride = versionName
output.versionCodeOverride = versionCodeOverride
@ -428,10 +446,6 @@ configurations {
// correct runtime classpath when invoked with Android Studio's built-in JUnit test runner.
// Success!
jnaForTest
// Robolectric, through `com.google.android.apps.common.testing.accessibility.framework`
// depends on an old version of protobuf that conflict with the Application Services one.
// See: https://github.com/mozilla/application-services/issues/2952
all*.exclude group: 'com.google.protobuf', module: 'protobuf-java'
}
dependencies {
@ -465,7 +479,6 @@ dependencies {
implementation Deps.mozilla_browser_icons
implementation Deps.mozilla_browser_menu
implementation Deps.mozilla_browser_menu2
implementation Deps.mozilla_browser_session
implementation Deps.mozilla_browser_session_storage
implementation Deps.mozilla_browser_state
implementation Deps.mozilla_browser_storage_sync
@ -538,6 +551,10 @@ dependencies {
debugImplementation Deps.leakcanary
forkDebugImplementation Deps.leakcanary
implementation Deps.androidx_compose_ui
implementation Deps.androidx_compose_ui_tooling
implementation Deps.androidx_compose_foundation
implementation Deps.androidx_compose_material
implementation Deps.androidx_legacy
implementation Deps.androidx_biometric
implementation Deps.androidx_paging
@ -588,6 +605,7 @@ dependencies {
androidTestImplementation Deps.androidx_junit
androidTestImplementation Deps.androidx_work_testing
androidTestImplementation Deps.androidx_benchmark_junit4
androidTestImplementation Deps.mockwebserver
testImplementation Deps.mozilla_support_test
testImplementation Deps.mozilla_support_test_libstate
@ -612,6 +630,11 @@ dependencies {
if (project.hasProperty("coverage")) {
tasks.withType(Test).configureEach {
jacoco.includeNoLocationClasses = true
jacoco.excludes = ['jdk.internal.*']
}
jacoco {
toolVersion = "0.8.7"
}
android.applicationVariants.all { variant ->
@ -779,14 +802,3 @@ ext.updateExtensionVersion = { task, extDir ->
expand(values)
}
}
tasks.register("updateAdsExtensionVersion", Copy) { task ->
updateExtensionVersion(task, 'src/main/assets/extensions/ads')
}
tasks.register("updateCookiesExtensionVersion", Copy) { task ->
updateExtensionVersion(task, 'src/main/assets/extensions/cookies')
}
preBuild.dependsOn "updateAdsExtensionVersion"
preBuild.dependsOn "updateCookiesExtensionVersion"

File diff suppressed because one or more lines are too long

@ -50,7 +50,7 @@
<!-- Performance: checks we'd like to eventually set to error. -->
<issue id="UseCompoundDrawables" severity="warning" />
<issue id="Overdraw" severity="warning" />
<issue id="UnusedResources" severity="warning" />
<issue id="UnusedResources" severity="error" />
<!-- Performance: checks that we're unsure of the value of that we might want to investigate. -->
<issue id="UnpackedNativeCode" severity="informational" />

File diff suppressed because it is too large Load Diff

@ -1,120 +0,0 @@
####################################################################################################
# Copy of `proguard-android-optimize.txt`.
# We need to _remove_ some of the configuration, so the only way to achieve that is to dump it here
# and modify it.
####################################################################################################
# This is a configuration file for ProGuard.
# http://proguard.sourceforge.net/index.html#manual/usage.html
#
# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with
# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and
# will be ignored by new version of the Android plugin for Gradle.
# Optimizations: If you don't want to optimize, use the proguard-android.txt configuration file
# instead of this one, which turns off the optimization flags.
# Adding optimization introduces certain risks, since for example not all optimizations performed by
# ProGuard works on all versions of Dalvik. The following flags turn off various optimizations
# known to have issues, but the list may not be complete or up to date. (The "arithmetic"
# optimization can be used if you are only targeting Android 2.0 or later.) Make sure you test
# thoroughly if you go this route.
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
-optimizationpasses 5
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-verbose
# Preserve some attributes that may be required for reflection.
-keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService
-keep public class com.google.android.vending.licensing.ILicensingService
-dontnote com.android.vending.licensing.ILicensingService
-dontnote com.google.vending.licensing.ILicensingService
-dontnote com.google.android.vending.licensing.ILicensingService
# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
-keepclasseswithmembernames class * {
native <methods>;
}
# Keep setters in Views so that animations can still work.
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
# We want to keep methods in Activity that could be used in the XML attribute onClick.
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keepclassmembers class * implements android.os.Parcelable {
public static final ** CREATOR;
}
-keepclassmembers class **.R$* {
public static <fields>;
}
# Preserve annotated Javascript interface methods.
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
# The support libraries contains references to newer platform versions.
# Don't warn about those in case this app is linking against an older
# platform version. We know about them, and they are safe.
-dontnote android.support.**
-dontnote androidx.**
-dontwarn android.support.**
-dontwarn androidx.**
# This class is deprecated, but remains for backward compatibility.
-dontwarn android.util.FloatMath
# Understand the @Keep support annotation.
-keep class android.support.annotation.Keep
-keep class androidx.annotation.Keep
-keep @android.support.annotation.Keep class * {*;}
-keep @androidx.annotation.Keep class * {*;}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <methods>;
}
-keepclasseswithmembers class * {
@androidx.annotation.Keep <methods>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <fields>;
}
-keepclasseswithmembers class * {
@androidx.annotation.Keep <fields>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <init>(...);
}
-keepclasseswithmembers class * {
@androidx.annotation.Keep <init>(...);
}
# These classes are duplicated between android.jar and org.apache.http.legacy.jar.
-dontnote org.apache.http.**
-dontnote android.net.http.**
# These classes are duplicated between android.jar and core-lambda-stubs.jar.
-dontnote java.lang.invoke.**

@ -32,17 +32,20 @@
-keep class org.mozilla.gecko.util.DebugConfig { *; }
####################################################################################################
# Force removal of slow Dispatchers.Main ServiceLoader
# kotlinx.coroutines: use the fast service loader to init MainDispatcherLoader by including a rule
# to rewrite this property to return true:
# https://github.com/Kotlin/kotlinx.coroutines/blob/8c98180f177bbe4b26f1ed9685a9280fea648b9c/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt#L19
#
# R8 is expected to optimize the default implementation to avoid a performance issue but a bug in R8
# as bundled with AGP v7.0.0 causes this optimization to fail so we use the fast service loader instead. See:
# https://github.com/mozilla-mobile/focus-android/issues/5102#issuecomment-897854121
#
# The fast service loader appears to be as performant as the R8 optimization so it's not worth the
# churn to later remove this workaround. If needed, the upstream fix is being handled in
# https://issuetracker.google.com/issues/196302685
####################################################################################################
# Allow R8 to optimize away the FastServiceLoader.
# Together with ServiceLoader optimization in R8
# this results in direct instantiation when loading Dispatchers.Main
-assumenosideeffects class kotlinx.coroutines.internal.MainDispatcherLoader {
boolean FAST_SERVICE_LOADER_ENABLED return false;
}
-assumenosideeffects class kotlinx.coroutines.internal.FastServiceLoader {
boolean ANDROID_DETECTED return true;
boolean FAST_SERVICE_LOADER_ENABLED return true;
}
####################################################################################################

@ -9,7 +9,7 @@ import androidx.navigation.NavController
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.request.RequestInterceptor
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ui.robots.appContext
import org.mozilla.fenix.helpers.TestHelper.appContext
import java.lang.ref.WeakReference
/**

@ -11,6 +11,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@ -76,10 +77,13 @@ class BaselinePingTest {
companion object {
@BeforeClass
@JvmStatic
@OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
fun setupOnce() {
val httpClient = ConceptFetchHttpUploader(lazy {
GeckoViewFetchClient(ApplicationProvider.getApplicationContext())
})
val httpClient = ConceptFetchHttpUploader(
lazy {
GeckoViewFetchClient(ApplicationProvider.getApplicationContext())
}
)
// Fenix does not initialize the Glean SDK in tests/debug builds, but this test
// requires Glean to be initialized so we need to do it manually. Additionally,
@ -149,8 +153,11 @@ class BaselinePingTest {
// sending the ping that was submitted on background. This can go away once bug 1634375
// is fixed.
device.pressRecentApps()
device.findObject(UiSelector().descriptionContains(
ApplicationProvider.getApplicationContext<Context>().getString(R.string.app_name)))
device.findObject(
UiSelector().descriptionContains(
ApplicationProvider.getApplicationContext<Context>().getString(R.string.app_name)
)
)
.click()
// Validate the received data.

@ -11,8 +11,8 @@ import androidx.test.rule.ActivityTestRule
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.helpers.TestHelper.appContext
import org.mozilla.fenix.onboarding.FenixOnboarding
import org.mozilla.fenix.ui.robots.appContext
/**
* A [org.junit.Rule] to handle shared test set up for tests on [HomeActivity].

@ -30,6 +30,7 @@ import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import kotlinx.coroutines.runBlocking
import mozilla.components.support.ktx.android.content.appName
import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.allOf
import org.mozilla.fenix.R
@ -41,10 +42,13 @@ import java.io.File
object TestHelper {
val packageName = InstrumentationRegistry.getInstrumentation().targetContext.packageName
val appContext: Context = InstrumentationRegistry.getInstrumentation().targetContext
val packageName: String = appContext.packageName
val appName = appContext.appName
fun scrollToElementByText(text: String): UiScrollable {
val appView = UiScrollable(UiSelector().scrollable(true))
appView.waitForExists(waitingTime)
appView.scrollTextIntoView(text)
return appView
}
@ -69,7 +73,7 @@ object TestHelper {
editor.apply()
}
fun restartApp(activity: HomeActivityTestRule) {
fun restartApp(activity: HomeActivityIntentTestRule) {
with(activity) {
finishActivity()
mDevice.waitForIdle()
@ -79,7 +83,7 @@ object TestHelper {
fun getPermissionAllowID(): String {
return when
(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
true -> "com.android.permissioncontroller"
false -> "com.android.packageinstaller"
}

@ -16,10 +16,10 @@ import java.net.URISyntaxException
*/
fun String.removePrefixesIgnoreCase(vararg prefixes: String): String {
var value = this
var lower = this.toLowerCase()
var lower = this.lowercase()
prefixes.forEach {
if (lower.startsWith(it.toLowerCase())) {
if (lower.startsWith(it.lowercase())) {
value = value.substring(it.length)
lower = lower.substring(it.length)
}

@ -0,0 +1,38 @@
/* 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 https://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.perf
import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/**
* To run this benchmark:
* - Comment out @Ignore: DO NOT COMMIT THIS!
* - See run instructions in app/benchmark.gradle
*
* See https://developer.android.com/studio/profile/benchmark#write-benchmark for how to write a
* real benchmark, including testing UI code. See
* https://developer.android.com/studio/profile/benchmark#what-to-benchmark for when jetpack
* microbenchmark is a good fit.
*/
@Ignore("This is a sample: we don't want it to run when we run all the tests")
@RunWith(AndroidJUnit4::class)
class SampleBenchmark {
@get:Rule
val benchmarkRule = BenchmarkRule()
@Test
fun additionBenchmark() = benchmarkRule.measureRepeated {
var i = 0
while (i < 10_000_000) {
i++
}
}
}

@ -58,7 +58,7 @@ private val failureMsgRecyclerViewConstraintLayoutChildren = getErrorMessage(
private val failureMsgNumberOfInflation = getErrorMessage(
shortName = "Number of inflation on start up doesn't match expected count",
implications = "The number of inflation can negatively impact start up time. Having more inflations" +
"will most likely mean we're adding extra work on the UI thread."
"will most likely mean we're adding extra work on the UI thread."
)
/**
* A performance test to limit the number of StrictMode suppressions and number of runBlocking used
@ -134,7 +134,7 @@ private fun countRecyclerViewConstraintLayoutChildren(view: View, parent: View?)
return if (view !is ViewGroup) {
viewValue
} else {
viewValue + view.children.sumBy { countRecyclerViewConstraintLayoutChildren(it, view) }
viewValue + view.children.sumOf { countRecyclerViewConstraintLayoutChildren(it, view) }
}
}

@ -83,14 +83,14 @@ class MenuScreenShotTest : ScreenshotTest() {
}.openLanguageSubMenu {
Screengrab.screenshot("SettingsSubMenuAccessibilityRobot_settings-language")
}.goBack {
// From about here we need to scroll up to ensure all settings options are visible.
// From about here we need to scroll up to ensure all settings options are visible.
}.openSetDefaultBrowserSubMenu {
Screengrab.screenshot("SettingsSubMenuDefaultBrowserRobot_settings-default-browser")
}.goBack {
// Disabled for Pixel 2
// }.openEnhancedTrackingProtectionSubMenu {
// Screengrab.screenshot("settings-enhanced-tp")
// }.goBack {
// Disabled for Pixel 2
// }.openEnhancedTrackingProtectionSubMenu {
// Screengrab.screenshot("settings-enhanced-tp")
// }.goBack {
}.openLoginsAndPasswordSubMenu {
Screengrab.screenshot("SettingsSubMenuLoginsAndPasswords-settings-logins-passwords")
}.goBack {
@ -176,7 +176,7 @@ class MenuScreenShotTest : ScreenshotTest() {
@Test
fun saveLoginPromptTest() {
val saveLoginTest =
TestAssetHelper.getSaveLoginAsset(mockWebServer)
TestAssetHelper.getSaveLoginAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(saveLoginTest.url) {
verifySaveLoginPromptIsShownNotSave()

@ -173,9 +173,11 @@ class SyncIntegrationTest {
// Useful functions for the tests
fun typeEmail() {
val emailInput = mDevice.findObject(UiSelector()
val emailInput = mDevice.findObject(
UiSelector()
.instance(0)
.className(EditText::class.java))
.className(EditText::class.java)
)
emailInput.waitForExists(TestAssetHelper.waitingTime)
val emailAddress = javaClass.classLoader!!.getResource("email.txt").readText()
@ -188,9 +190,11 @@ class SyncIntegrationTest {
}
fun typePassword() {
val passwordInput = mDevice.findObject(UiSelector()
val passwordInput = mDevice.findObject(
UiSelector()
.instance(0)
.className(EditText::class.java))
.className(EditText::class.java)
)
val passwordValue = javaClass.classLoader!!.getResource("password.txt").readText()
passwordInput.setText(passwordValue)

@ -0,0 +1,262 @@
/* 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.helpers.TestAssetHelper.getGenericAsset
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.tabDrawer
/**
* Tests for verifying basic functionality of tab collections
*
*/
class CollectionTest {
/* ktlint-disable no-blank-line-before-rbrace */
// This imposes unreadable grouping.
private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
private lateinit var mockWebServer: MockWebServer
private val firstCollectionName = "testcollection_1"
private val secondCollectionName = "testcollection_2"
@get:Rule
val activityTestRule = HomeActivityTestRule()
@Before
fun setUp() {
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
@Test
// open a webpage, and add currently opened tab to existing collection
fun mainMenuSaveToExistingCollection() {
val firstWebPage = getGenericAsset(mockWebServer, 1)
val secondWebPage = getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
}.openTabDrawer {
createCollection(firstWebPage.title, firstCollectionName)
verifySnackBarText("Collection saved!")
}.closeTabDrawer {}
navigationToolbar {
}.enterURLAndEnterToBrowser(secondWebPage.url) {
verifyPageContent(secondWebPage.content)
}.openThreeDotMenu {
}.openSaveToCollection {
}.selectExistingCollection(firstCollectionName) {
verifySnackBarText("Tab saved!")
}.goToHomescreen {
}.expandCollection(firstCollectionName) {
verifyTabSavedInCollection(firstWebPage.title)
verifyTabSavedInCollection(secondWebPage.title)
}
}
@Test
fun verifyAddTabButtonOfCollectionMenu() {
val firstWebPage = getGenericAsset(mockWebServer, 1)
val secondWebPage = getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
}.openTabDrawer {
createCollection(firstWebPage.title, firstCollectionName)
verifySnackBarText("Collection saved!")
closeTab()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(secondWebPage.url) {
}.goToHomescreen {
}.expandCollection(firstCollectionName) {
clickCollectionThreeDotButton()
selectAddTabToCollection()
verifyTabsSelectedCounterText(1)
saveTabsSelectedForCollection()
verifySnackBarText("Tab saved!")
verifyTabSavedInCollection(secondWebPage.title)
}
}
@Test
fun renameCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openTabDrawer {
createCollection(webPage.title, firstCollectionName)
verifySnackBarText("Collection saved!")
}.closeTabDrawer {
}.goToHomescreen {
}.expandCollection(firstCollectionName) {
clickCollectionThreeDotButton()
selectRenameCollection()
}.typeCollectionNameAndSave("renamed_collection") {}
homeScreen {
verifyCollectionIsDisplayed("renamed_collection")
}
}
@Test
fun createSecondCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openTabDrawer {
createCollection(webPage.title, firstCollectionName)
verifySnackBarText("Collection saved!")
createCollection(webPage.title, secondCollectionName, false)
verifySnackBarText("Collection saved!")
}.closeTabDrawer {
}.goToHomescreen {}
homeScreen {
verifyCollectionIsDisplayed(firstCollectionName)
verifyCollectionIsDisplayed(secondCollectionName)
}
}
@Test
fun removeTabFromCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openTabDrawer {
createCollection(webPage.title, firstCollectionName)
verifySnackBarText("Collection saved!")
}.closeTabDrawer {
}.goToHomescreen {
}.expandCollection(firstCollectionName) {
removeTabFromCollection(webPage.title)
verifyTabSavedInCollection(webPage.title, false)
}
// To add this step when https://github.com/mozilla-mobile/fenix/issues/13177 is fixed
// homeScreen {
// verifyCollectionIsDisplayed(firstCollectionName, false)
// }
}
@Test
@Ignore("To be fixed in https://github.com/mozilla-mobile/fenix/issues/20702")
fun swipeToRemoveTabFromCollectionTest() {
val firstWebPage = getGenericAsset(mockWebServer, 1)
val secondWebPage = getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
}.openTabDrawer {
createCollection(firstWebPage.title, firstCollectionName)
verifySnackBarText("Collection saved!")
closeTab()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(secondWebPage.url) {
}.openThreeDotMenu {
}.openSaveToCollection {
}.selectExistingCollection(firstCollectionName) {
}.goToHomescreen {}
homeScreen {
}.expandCollection(firstCollectionName) {
swipeCollectionItemLeft(firstWebPage.title)
verifyTabSavedInCollection(firstWebPage.title, false)
swipeCollectionItemRight(secondWebPage.title)
verifyTabSavedInCollection(secondWebPage.title, false)
}
// To add this step when https://github.com/mozilla-mobile/fenix/issues/13177 is fixed
// homeScreen {
// verifyCollectionIsDisplayed(firstCollectionName, false)
// }
}
@Test
fun selectTabOnLongTapTest() {
val firstWebPage = getGenericAsset(mockWebServer, 1)
val secondWebPage = getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
}.openTabDrawer {
}.openNewTab {
}.submitQuery(secondWebPage.url.toString()) {
}.openTabDrawer {
longClickTab(firstWebPage.title)
verifyTabsMultiSelectionCounter(1)
selectTab(secondWebPage.title)
verifyTabsMultiSelectionCounter(2)
}.clickSaveCollection {
typeCollectionNameAndSave(firstCollectionName)
verifySnackBarText("Tabs saved!")
}
tabDrawer {
}.closeTabDrawer {
}.goToHomescreen {
}.expandCollection(firstCollectionName) {
verifyTabSavedInCollection(firstWebPage.title)
verifyTabSavedInCollection(secondWebPage.title)
}
}
@Test
fun navigateBackInCollectionFlowTest() {
val webPage = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openTabDrawer {
createCollection(webPage.title, firstCollectionName)
verifySnackBarText("Collection saved!")
}.closeTabDrawer {
}.openThreeDotMenu {
}.openSaveToCollection {
verifySelectCollectionScreen()
goBackInCollectionFlow()
}
browserScreen {
}.openThreeDotMenu {
}.openSaveToCollection {
verifySelectCollectionScreen()
clickAddNewCollection()
verifyCollectionNameTextField()
goBackInCollectionFlow()
verifySelectCollectionScreen()
goBackInCollectionFlow()
}
// verify the browser layout is visible
browserScreen {
verifyMenuButton()
}
}
}

@ -1,266 +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.espresso.NoMatchingViewException
//import androidx.test.platform.app.InstrumentationRegistry
//import androidx.test.uiautomator.By
//import androidx.test.uiautomator.UiDevice
//import androidx.test.uiautomator.Until
//import okhttp3.mockwebserver.MockWebServer
//import org.junit.After
//import org.junit.Before
//import org.junit.Ignore
//import org.junit.Rule
//import org.junit.Test
//import org.mozilla.fenix.helpers.AndroidAssetDispatcher
//import org.mozilla.fenix.helpers.HomeActivityTestRule
//import org.mozilla.fenix.helpers.TestAssetHelper
//import org.mozilla.fenix.ui.robots.homeScreen
//import org.mozilla.fenix.ui.robots.navigationToolbar
//
///**
// * Tests for verifying basic functionality of tab collection
// *
// */
//
//class CollectionTest {
// /* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
//
// private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
// private lateinit var mockWebServer: MockWebServer
// private val firstCollectionName = "testcollection_1"
// private val secondCollectionName = "testcollection_2"
//
// @get:Rule
// val activityTestRule = HomeActivityTestRule()
//
// @Before
// fun setUp() {
// mockWebServer = MockWebServer().apply {
// setDispatcher(AndroidAssetDispatcher())
// start()
// }
// }
//
// @After
// fun tearDown() {
// mockWebServer.shutdown()
// }
//
// @Ignore("Intermittent failures, see: https://github.com/mozilla-mobile/fenix/issues/10587")
// @Test
// // open a webpage, and add currently opened tab to existing collection
// fun addTabToExistingCollectionTest() {
// val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
// val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
//
// createCollection(firstCollectionName)
//
// homeScreen {
//// verifyExistingTabList()
// closeTab()
// }.openNavigationToolbar {
// }.enterURLAndEnterToBrowser(secondWebPage.url) {
// verifyPageContent(secondWebPage.content)
// }.openThreeDotMenu {
// clickBrowserViewSaveCollectionButton()
// }.selectExistingCollection(firstCollectionName) {
// verifySnackBarText("Tab saved!")
// }.openHomeScreen {
//// verifyExistingTabList()
// expandCollection(firstCollectionName)
// verifyItemInCollectionExists(firstWebPage.title)
// verifyItemInCollectionExists(secondWebPage.title)
// }
// }
//
// @Ignore("Intermittent failures, see: https://github.com/mozilla-mobile/fenix/issues/10587")
// @Test
// fun collectionMenuAddTabButtonTest() {
// val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
//
// createCollection(firstCollectionName)
//
// homeScreen {
// closeTab()
// }.openNavigationToolbar {
// }.enterURLAndEnterToBrowser(secondWebPage.url) {
// }.openHomeScreen {
// expandCollection(firstCollectionName)
// clickCollectionThreeDotButton()
// selectAddTabToCollection()
// verifyTabsSelectedCounterText(1)
// saveTabsSelectedForCollection()
// verifySnackBarText("Tab saved!")
// verifyItemInCollectionExists(secondWebPage.title)
// }
// }
//
// @Ignore("Intermittent failures, see: https://github.com/mozilla-mobile/fenix/issues/10587")
// @Test
// fun renameCollectionTest() {
// createCollection(firstCollectionName)
//
// homeScreen {
// // On homeview, tap the 3-dot button to expand, select rename, rename collection
// expandCollection(firstCollectionName)
// clickCollectionThreeDotButton()
// selectRenameCollection()
// typeCollectionName("renamed_collection")
// verifyCollectionIsDisplayed("renamed_collection")
// }
// }
//
// @Ignore("Intermittent failures, see: https://github.com/mozilla-mobile/fenix/issues/10587")
// @Test
// fun createCollectionFromTabTest() {
// val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
//
// createCollection(firstCollectionName)
// homeScreen {
// }.openTabDrawer {
// verifyExistingOpenTabs(firstWebPage.title)
// }.openHomeScreen {
// try {
// verifyCollectionIsDisplayed(firstCollectionName)
// } catch (e: NoMatchingViewException) {
// scrollToElementByText(firstCollectionName)
// }
// }
// }
//
// @Ignore("Intermittent failures, see: https://github.com/mozilla-mobile/fenix/issues/10587")
// @Test
// fun removeTabFromCollectionTest() {
// val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
//
// createCollection(firstCollectionName)
// homeScreen {
// closeTab()
// expandCollection(firstCollectionName)
// removeTabFromCollection(webPage.title)
// verifyItemInCollectionExists(webPage.title, false)
// }
//
// createCollection(firstCollectionName)
// homeScreen {
// closeTab()
// expandCollection(firstCollectionName)
// swipeCollectionItemLeft(webPage.title)
// verifyItemInCollectionExists(webPage.title, false)
// }
//
// createCollection(firstCollectionName)
// homeScreen {
// closeTab()
// expandCollection(firstCollectionName)
// swipeCollectionItemRight(webPage.title)
// verifyItemInCollectionExists(webPage.title, false)
// }
// }
//
// @Ignore("Intermittent failures, see: https://github.com/mozilla-mobile/fenix/issues/10587")
// @Test
// fun selectTabOnLongTapTest() {
// val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
// val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
//
// navigationToolbar {
// }.enterURLAndEnterToBrowser(firstWebPage.url) {
// }.openHomeScreen {
// }.openNavigationToolbar {
// }.enterURLAndEnterToBrowser(secondWebPage.url) {
// }.openHomeScreen {
// longTapSelectTab(firstWebPage.title)
// verifySelectTabsView()
// verifyTabsSelectedCounterText(1)
// selectTabForCollection(secondWebPage.title)
// verifyTabsSelectedCounterText(2)
// saveTabsSelectedForCollection()
// typeCollectionName(firstCollectionName)
// verifySnackBarText("Tabs saved!")
//// closeTabViaXButton(firstWebPage.title)
//// closeTabViaXButton(secondWebPage.title)
// expandCollection(firstCollectionName)
// verifyItemInCollectionExists(firstWebPage.title)
// verifyItemInCollectionExists(secondWebPage.title)
// }
// }
//
// @Ignore("Intermittent failures, see: https://github.com/mozilla-mobile/fenix/issues/10587")
// @Test
// fun tabsOverflowMenuSaveCollectionTest() {
// val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
// val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
//
// navigationToolbar {
// }.enterURLAndEnterToBrowser(firstWebPage.url) {
// }.openHomeScreen {
// }.openNavigationToolbar {
// }.enterURLAndEnterToBrowser(secondWebPage.url) {
// }.openHomeScreen {
// }.openTabsListThreeDotMenu {
// verifySaveCollection()
// }.clickOpenTabsMenuSaveCollection {
// verifySelectTabsView()
// verifyTabsSelectedCounterText(0)
// selectAllTabsForCollection()
// verifyTabsSelectedCounterText(2)
// saveTabsSelectedForCollection()
// typeCollectionName(firstCollectionName)
//// closeTabViaXButton(firstWebPage.title)
//// closeTabViaXButton(secondWebPage.title)
// expandCollection(firstCollectionName)
// verifyItemInCollectionExists(firstWebPage.title)
// verifyItemInCollectionExists(secondWebPage.title)
// }
// }
//
// @Ignore("Intermittent failures, see: https://github.com/mozilla-mobile/fenix/issues/10587")
// @Test
// fun navigateBackInCollectionFlowTest() {
// val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
//
// createCollection(firstCollectionName)
// navigationToolbar {
// }.enterURLAndEnterToBrowser(secondWebPage.url) {
// }.openHomeScreen {
// longTapSelectTab(secondWebPage.title)
// verifySelectTabsView()
// saveTabsSelectedForCollection()
// verifySelectCollectionView()
// clickAddNewCollection()
// verifyNameCollectionView()
// goBackCollectionFlow()
// verifySelectCollectionView()
// goBackCollectionFlow()
// verifySelectTabsView()
// goBackCollectionFlow()
// verifyHomeComponent()
// }
// }
//
// private fun createCollection(collectionName: String, firstCollection: Boolean = true) {
// val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
//
// navigationToolbar {
// }.enterURLAndEnterToBrowser(firstWebPage.url) {
// verifyPageContent(firstWebPage.content)
// }.openThreeDotMenu {
// clickBrowserViewSaveCollectionButton()
// if (!firstCollection)
// clickAddNewCollection()
//
// }.typeCollectionName(collectionName) {
// verifySnackBarText("Tab saved!")
// }.openHomeScreen {
// mDevice.wait(
// Until.findObject(By.text(collectionName)),
// TestAssetHelper.waitingTime
// )
// }
// }
//}

@ -108,7 +108,7 @@ class DeepLinkTest {
fun openSettingsLogins() {
robot.openSettingsLogins {
verifyDefaultView()
verifyDefaultValueAutofillLogins()
verifyDefaultValueAutofillLogins(InstrumentationRegistry.getInstrumentation().targetContext)
}
}

@ -74,6 +74,8 @@ class HistoryTest {
}
@Test
// Test running on beta/release builds in CI:
// caution when making changes to it, so they don't block the builds
fun visitedUrlHistoryTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)

@ -94,6 +94,8 @@ class NavigationToolbarTest {
}
@Test
// Test running on beta/release builds in CI:
// caution when making changes to it, so they don't block the builds
fun visitURLTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)

@ -34,6 +34,8 @@ class NoNetworkAccessStartupTests {
}
@Test
// Test running on beta/release builds in CI:
// caution when making changes to it, so they don't block the builds
// Based on STR from https://github.com/mozilla-mobile/fenix/issues/16886
fun noNetworkConnectionStartupTest() {
setNetworkEnabled(false)

@ -96,7 +96,7 @@ class SettingsAddonsTest {
acceptInstallAddon()
verifyDownloadAddonPrompt(addonName, activityTestRule)
}
}
}
// Opens the addons settings menu, installs an addon, then uninstalls

@ -161,7 +161,6 @@ class SettingsBasicsTest {
}
}
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/18986")
@Test
fun changeAccessibiltySettings() {
// Goes through the settings and changes the default text on a webpage, then verifies if the text has changed.
@ -194,16 +193,4 @@ class SettingsBasicsTest {
verifyMenuItemsAreDisabled()
}
}
@Test
fun changeDefaultBrowserSetting() {
// Opens settings and toggles the default browser setting to on. The device settings open and allows the user to set a default browser.
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyDefaultBrowserIsDisaled()
clickDefaultBrowserSwitch()
verifyAndroidDefaultAppsMenuAppears()
}
}
}

@ -13,7 +13,7 @@ import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.openAppFromExternalLink
@ -36,7 +36,7 @@ class SettingsPrivacyTest {
private val pageShortcutName = "TestShortcut"
@get:Rule
val activityTestRule = HomeActivityTestRule()
val activityTestRule = HomeActivityIntentTestRule()
@Before
fun setUp() {
@ -176,7 +176,7 @@ class SettingsPrivacyTest {
TestHelper.scrollToElementByText("Logins and passwords")
}.openLoginsAndPasswordSubMenu {
verifyDefaultView()
verifyDefaultValueAutofillLogins()
verifyDefaultValueAutofillLogins(InstrumentationRegistry.getInstrumentation().targetContext)
verifyDefaultValueExceptions()
}.openSavedLogins {
verifySecurityPromptForLogins()
@ -270,22 +270,24 @@ class SettingsPrivacyTest {
}
@Test
@Ignore("See: https://github.com/mozilla-mobile/fenix/issues/10915")
fun openExternalLinksInPrivateTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
setOpenLinksInPrivateOn()
openAppFromExternalLink(defaultWebPage.url.toString())
openAppFromExternalLink(firstWebPage.url.toString())
browserScreen {
}.openTabDrawer {
verifyPrivateModeSelected()
}.openNewTab { }.dismissSearchBar { }
}.closeTabDrawer {
}.goToHomescreen { }
setOpenLinksInPrivateOff()
openAppFromExternalLink(defaultWebPage.url.toString())
// We need to open a different link, otherwise it will open the same session
openAppFromExternalLink(secondWebPage.url.toString())
browserScreen {
}.openTabDrawer {
@ -294,7 +296,6 @@ class SettingsPrivacyTest {
}
@Test
@Ignore("See: https://github.com/mozilla-mobile/fenix/issues/10915")
fun launchPageShortcutInPrivateModeTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -307,7 +308,18 @@ class SettingsPrivacyTest {
addShortcutName(pageShortcutName)
clickAddShortcutButton()
clickAddAutomaticallyButton()
}.openHomeScreenShortcut(pageShortcutName) {
}
mDevice.waitForIdle()
// We need to close the existing tab here, to open a different session
restartApp(activityTestRule)
browserScreen {
}.openTabDrawer {
closeTab()
}
addToHomeScreen {
}.searchAndOpenHomeScreenShortcut(pageShortcutName) {
}.openTabDrawer {
verifyPrivateModeSelected()
}
@ -328,8 +340,7 @@ class SettingsPrivacyTest {
clickAddShortcutButton()
clickAddAutomaticallyButton()
}.openHomeScreenShortcut(pageShortcutName) {
}.openTabDrawer {
}.openNewTab { }.dismissSearchBar { }
}.goToHomescreen { }
setOpenLinksInPrivateOff()
restartApp(activityTestRule)
@ -339,8 +350,7 @@ class SettingsPrivacyTest {
}.searchAndOpenHomeScreenShortcut(pageShortcutName) {
}.openTabDrawer {
verifyNormalModeSelected()
}.openNewTab {
}.dismissSearchBar {
}.closeTabDrawer {
}.openThreeDotMenu {
}.openSettings {
}.openPrivateBrowsingSubMenu {

@ -20,23 +20,25 @@ import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.IntentReceiverActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.TestHelper.createCustomTabIntent
import org.mozilla.fenix.helpers.TestHelper.deleteDownloadFromStorage
import org.mozilla.fenix.helpers.TestHelper.restartApp
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.clickTabCrashedRestoreButton
import org.mozilla.fenix.ui.robots.clickUrlbar
import org.mozilla.fenix.ui.robots.collectionRobot
import org.mozilla.fenix.ui.robots.customTabScreen
import org.mozilla.fenix.ui.robots.dismissTrackingOnboarding
import org.mozilla.fenix.ui.robots.downloadRobot
@ -44,7 +46,12 @@ 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.notificationShade
import org.mozilla.fenix.ui.robots.openEditURLView
import org.mozilla.fenix.ui.robots.searchScreen
import org.mozilla.fenix.ui.robots.tabDrawer
import org.mozilla.fenix.ui.util.FRENCH_LANGUAGE_HEADER
import org.mozilla.fenix.ui.util.FRENCH_SYSTEM_LOCALE_OPTION
import org.mozilla.fenix.ui.util.ROMANIAN_LANGUAGE_HEADER
import org.mozilla.fenix.ui.util.STRING_ONBOARDING_TRACKING_PROTECTION_HEADER
/**
@ -63,6 +70,7 @@ class SmokeTest {
private val downloadFileName = "Globe.svg"
private val collectionName = "First Collection"
private var bookmarksListIdlingResource: RecyclerViewIdlingResource? = null
private var localeListIdlingResource: RecyclerViewIdlingResource? = null
private val customMenuItem = "TestMenuItem"
// This finds the dialog fragment child of the homeFragment, otherwise the awesomeBar would return null
@ -75,7 +83,7 @@ class SmokeTest {
}
@get:Rule
val activityTestRule = HomeActivityTestRule()
val activityTestRule = HomeActivityIntentTestRule()
private lateinit var browserStore: BrowserStore
@get: Rule
@ -130,6 +138,10 @@ class SmokeTest {
if (readerViewNotification != null) {
IdlingRegistry.getInstance().unregister(readerViewNotification)
}
if (localeListIdlingResource != null) {
IdlingRegistry.getInstance().unregister(localeListIdlingResource)
}
}
// Verifies the first run onboarding screen
@ -249,23 +261,16 @@ class SmokeTest {
// Verifies the Synced tabs menu or Sync Sign In menu opens from a tab's 3 dot menu.
// The test is assuming we are NOT signed in.
fun openMainMenuSyncItemTest() {
if (FeatureFlags.tabsTrayRewrite) {
homeScreen {
}.openThreeDotMenu {
}.openSyncSignIn {
verifySyncSignInMenuHeader()
}
} else {
homeScreen {
}.openThreeDotMenu {
}.openSyncedTabs {
verifySyncedTabsMenuHeader()
}
homeScreen {
}.openThreeDotMenu {
}.openSyncSignIn {
verifySyncSignInMenuHeader()
}
}
// Could be removed when more smoke tests from the Settings category are added
@Test
// Test running on beta/release builds in CI:
// caution when making changes to it, so they don't block the builds
// Verifies the Settings menu opens from a tab's 3 dot menu
fun openMainMenuSettingsItemTest() {
homeScreen {
@ -414,9 +419,9 @@ class SmokeTest {
}
}
@Ignore("Failing, see https://github.com/mozilla-mobile/fenix/issues/18647")
@Test
fun customTrackingProtectionSettingsTest() {
val genericWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val trackingPage = TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer)
homeScreen {
@ -429,10 +434,14 @@ class SmokeTest {
}.goBackToHomeScreen {}
navigationToolbar {
}.openTrackingProtectionTestPage(trackingPage.url, true) {}
// browsing a basic page to allow GV to load on a fresh run
}.enterURLAndEnterToBrowser(genericWebPage.url) {
}.openNavigationToolbar {
}.openTrackingProtectionTestPage(trackingPage.url, true) {
dismissTrackingOnboarding()
}
enhancedTrackingProtection {
dismissTrackingOnboarding()
}.openEnhancedTrackingProtectionSheet {
verifyTrackingCookiesBlocked()
verifyCryptominersBlocked()
@ -511,6 +520,8 @@ class SmokeTest {
}
@Test
// Test running on beta/release builds in CI:
// caution when making changes to it, so they don't block the builds
// Goes through the settings and changes the search suggestion toggle, then verifies it changes.
fun toggleSearchSuggestions() {
@ -564,6 +575,7 @@ class SmokeTest {
@Test
// Saves a login, then changes it and verifies the update
@Ignore("To be fixed in https://github.com/mozilla-mobile/fenix/issues/20702")
fun updateSavedLoginTest() {
val saveLoginTest =
TestAssetHelper.getSaveLoginAsset(mockWebServer)
@ -590,7 +602,7 @@ class SmokeTest {
verifySavedLoginFromPrompt()
viewSavedLoginDetails()
revealPassword()
verifyPasswordSaved("test")
verifyPasswordSaved("test") // failing here locally
}
}
@ -655,12 +667,6 @@ class SmokeTest {
enhancedTrackingProtection {
verifyEnhancedTrackingProtectionNotice()
}.closeNotificationPopup {}
browserScreen {
}.openThreeDotMenu {
}.openReportSiteIssue {
verifyUrl("webcompat.com/issues/new")
}
}
@Test
@ -900,10 +906,13 @@ class SmokeTest {
mDevice.waitForIdle()
}.goToHomescreen {
}.clickSaveTabsToCollectionButton {
selectTab(firstWebPage.title)
longClickTab(firstWebPage.title)
selectTab(secondWebPage.title)
clickSaveCollection()
typeCollectionName(collectionName)
}.clickSaveCollection {
typeCollectionNameAndSave(collectionName)
}
tabDrawer {
verifySnackBarText("Collection saved!")
snackBarButtonClick("VIEW")
}
@ -914,7 +923,6 @@ class SmokeTest {
}
}
@Ignore("Disabling until re-implemented by #19090")
@Test
fun verifyExpandedCollectionItemsTest() {
val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -929,14 +937,16 @@ class SmokeTest {
homeScreen {
verifyCollectionIsDisplayed(collectionName)
verifyCollectionIcon()
expandCollection(collectionName)
}.expandCollection(collectionName) {
verifyTabSavedInCollection(webPage.title)
verifyCollectionTabLogo()
verifyCollectionTabUrl()
verifyShareCollectionButtonIsVisible(true)
verifyCollectionMenuIsVisible(true)
verifyCollectionItemRemoveButtonIsVisible(webPage.title, true)
collapseCollection(collectionName)
}.collapseCollection(collectionName) {}
collectionRobot {
verifyTabSavedInCollection(webPage.title, false)
verifyShareCollectionButtonIsVisible(false)
verifyCollectionMenuIsVisible(false)
@ -951,11 +961,12 @@ class SmokeTest {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openTabDrawer {
createCollection(webPage.title, collectionName)
verifySnackBarText("Collection saved!")
closeTab()
}
browserScreen {
}.goToHomescreen {
expandCollection(collectionName)
homeScreen {
}.expandCollection(collectionName) {
clickCollectionThreeDotButton()
selectOpenTabs()
}
@ -964,7 +975,6 @@ class SmokeTest {
}
}
@Ignore("Disabling until re-implemented by #19090")
@Test
fun shareCollectionTest() {
val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -975,15 +985,20 @@ class SmokeTest {
createCollection(webPage.title, collectionName)
snackBarButtonClick("VIEW")
}
homeScreen {
expandCollection(collectionName)
}.expandCollection(collectionName) {
clickShareCollectionButton()
}
homeScreen {
verifyShareTabsOverlay()
}
}
@Ignore("Disabling until re-implemented by #19090")
@Test
// Test running on beta/release builds in CI:
// caution when making changes to it, so they don't block the builds
fun deleteCollectionTest() {
val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -993,11 +1008,15 @@ class SmokeTest {
createCollection(webPage.title, collectionName)
snackBarButtonClick("VIEW")
}
homeScreen {
expandCollection(collectionName)
}.expandCollection(collectionName) {
clickCollectionThreeDotButton()
selectDeleteCollection()
confirmDeleteCollection()
}
homeScreen {
verifyNoCollectionsText()
}
}
@ -1098,13 +1117,14 @@ class SmokeTest {
verifyExistingOpenTabs("Test_Page_1")
verifyCloseTabsButton("Test_Page_1")
verifyOpenedTabThumbnail()
verifyBrowserTabsTrayURL("localhost")
verifyTabTrayOverflowMenu(true)
verifyNewTabButton()
}
}
@Test
// Test running on beta/release builds in CI:
// caution when making changes to it, so they don't block the builds
fun noHistoryInPrivateBrowsingTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -1132,11 +1152,16 @@ class SmokeTest {
verifyAddPrivateBrowsingShortcutButton()
clickAddPrivateBrowsingShortcutButton()
clickAddAutomaticallyButton()
}.openHomeScreenShortcut("Private Firefox Preview") {
}.openHomeScreenShortcut("Private $appName") {}
searchScreen {
verifySearchView()
}.dismissSearchBar {
verifyPrivateSessionMessage()
}
}
@Test
@Ignore("To be re-enabled later. See https://github.com/mozilla-mobile/fenix/issues/20716")
fun mainMenuInstallPWATest() {
val pwaPage = "https://rpappalax.github.io/testapp/"
@ -1206,6 +1231,7 @@ class SmokeTest {
}
}
@Ignore("Test failure caused by: https://github.com/mozilla-mobile/fenix/issues/19964")
@Test
fun restoreTabCrashedReporterTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -1234,9 +1260,7 @@ class SmokeTest {
)
customTabScreen {
browserScreen {
verifyPageContent(customTabPage.content)
}
verifyCustomTabCloseButton()
}.openMainMenu {
verifyPoweredByTextIsDisplayed()
verifyCustomMenuItem(customMenuItem)
@ -1261,9 +1285,7 @@ class SmokeTest {
)
customTabScreen {
browserScreen {
verifyPageContent(customTabPage.content)
}
verifyCustomTabCloseButton()
}.openMainMenu {
}.clickOpenInBrowserButton {
verifyTabCounter("1")
@ -1320,4 +1342,174 @@ class SmokeTest {
assertPlaybackState(browserStore, MediaSession.PlaybackState.PAUSED)
}
}
@Test
// For API>23
// Verifies the default browser switch opens the system default apps menu.
fun changeDefaultBrowserSetting() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyDefaultBrowserIsDisaled()
clickDefaultBrowserSwitch()
verifyAndroidDefaultAppsMenuAppears()
}
}
@Test
fun copyTextTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
longClickAndCopyText("content")
}.openNavigationToolbar {
openEditURLView()
}
searchScreen {
clickClearButton()
longClickToolbar()
clickPasteText()
verifyPastedToolbarText("content")
}
}
@Test
fun selectAllAndCopyTextTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
longClickAndCopyText("content", true)
}.openNavigationToolbar {
openEditURLView()
}
searchScreen {
clickClearButton()
longClickToolbar()
clickPasteText()
verifyPastedToolbarText("Page content: 1")
}
}
@Test
fun switchLanguageTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openLanguageSubMenu {
localeListIdlingResource =
RecyclerViewIdlingResource(
activityTestRule.activity.findViewById(R.id.locale_list),
2
)
IdlingRegistry.getInstance().register(localeListIdlingResource)
selectLanguage("Romanian")
verifyLanguageHeaderIsTranslated(ROMANIAN_LANGUAGE_HEADER)
selectLanguage("Français")
verifyLanguageHeaderIsTranslated(FRENCH_LANGUAGE_HEADER)
selectLanguage(FRENCH_SYSTEM_LOCALE_OPTION)
verifyLanguageHeaderIsTranslated("Language")
IdlingRegistry.getInstance().unregister(localeListIdlingResource)
}
}
@Test
fun goToHomeScreenBottomToolbarTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
mDevice.waitForIdle()
}.goToHomescreen {
verifyHomeScreen()
}
}
@Test
fun goToHomeScreenTopToolbarTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openCustomizeSubMenu {
clickTopToolbarToggle()
}.goBack {
}.goBack {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
mDevice.waitForIdle()
}.goToHomescreen {
verifyHomeScreen()
}
}
@Test
fun goToHomeScreenBottomToolbarPrivateModeTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
togglePrivateBrowsingModeOnOff()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
mDevice.waitForIdle()
}.goToHomescreen {
verifyHomeScreen()
}
}
@Test
fun goToHomeScreenTopToolbarPrivateModeTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
togglePrivateBrowsingModeOnOff()
}.openThreeDotMenu {
}.openSettings {
}.openCustomizeSubMenu {
clickTopToolbarToggle()
}.goBack {
}.goBack {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
mDevice.waitForIdle()
}.goToHomescreen {
verifyHomeScreen()
}
}
@Test
fun startOnHomeSettingsMenuItemsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openTabsSubMenu {
verifyStartOnHomeOptions()
}
}
@Test
fun alwaysStartOnHomeTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openSettings {
}.openTabsSubMenu {
clickAlwaysStartOnHomeToggle()
}
restartApp(activityTestRule)
homeScreen {
verifyHomeScreen()
}
}
}

@ -9,7 +9,6 @@ import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.ui.robots.homeScreen
@ -49,11 +48,7 @@ class ThreeDotMenuMainTest {
verifyHistoryButton()
verifyDownloadsButton()
verifyAddOnsButton()
if (FeatureFlags.tabsTrayRewrite) {
verifySyncSignInButton()
} else {
verifySyncedTabsButton()
}
verifySyncSignInButton()
verifyDesktopSite()
verifyWhatsNewButton()
verifyHelpButton()

@ -44,19 +44,19 @@ private fun deviceName() = Espresso.onView(CoreMatchers.allOf(ViewMatchers.withT
private fun disconnectButton() = Espresso.onView(CoreMatchers.allOf(ViewMatchers.withId(R.id.signOutDisconnect)))
private fun assertBookmarksCheckbox() = bookmarksCheckbox().check(
ViewAssertions.matches(
ViewMatchers.withEffectiveVisibility(
ViewMatchers.Visibility.VISIBLE
)
ViewAssertions.matches(
ViewMatchers.withEffectiveVisibility(
ViewMatchers.Visibility.VISIBLE
)
)
)
private fun assertHistoryCheckbox() = historyCheckbox().check(
ViewAssertions.matches(
ViewMatchers.withEffectiveVisibility(
ViewMatchers.Visibility.VISIBLE
)
ViewAssertions.matches(
ViewMatchers.withEffectiveVisibility(
ViewMatchers.Visibility.VISIBLE
)
)
)
private fun assertSignOutButton() = signOutButton().check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))

@ -91,8 +91,12 @@ fun addToHomeScreen(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenR
private fun shortcutNameField() = onView(withId(R.id.shortcut_text))
private fun assertShortcutNameField(expectedText: String) {
onView(allOf(withId(R.id.shortcut_text),
withText(expectedText)))
onView(
allOf(
withId(R.id.shortcut_text),
withText(expectedText)
)
)
.check(matches(isCompletelyDisplayed()))
}

@ -105,8 +105,10 @@ class BookmarksRobot {
fun verifySelectDefaultFolderSnackBarText() = assertSnackBarText("Cant edit default folders")
fun verifyCurrentFolderTitle(title: String) {
mDevice.findObject(UiSelector().resourceId("$packageName:id/navigationToolbar")
.textContains(title))
mDevice.findObject(
UiSelector().resourceId("$packageName:id/navigationToolbar")
.textContains(title)
)
.waitForExists(waitingTime)
onView(
@ -119,8 +121,10 @@ class BookmarksRobot {
}
fun waitForBookmarksFolderContentToExist(parentFolderName: String, childFolderName: String) {
mDevice.findObject(UiSelector().resourceId("$packageName:id/navigationToolbar")
.textContains(parentFolderName))
mDevice.findObject(
UiSelector().resourceId("$packageName:id/navigationToolbar")
.textContains(parentFolderName)
)
.waitForExists(waitingTime)
mDevice.waitNotNull(Until.findObject(By.text(childFolderName)), waitingTime)

@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@file:Suppress("TooManyFunctions")
@file:Suppress("TooManyFunctions", "TooGenericExceptionCaught")
package org.mozilla.fenix.ui.robots
@ -23,6 +23,7 @@ import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
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
import androidx.test.espresso.matcher.ViewMatchers.withResourceName
@ -333,6 +334,59 @@ class BrowserRobot {
element.click(LONG_CLICK_DURATION)
}
fun longClickAndCopyText(expectedText: String, selectAll: Boolean = false) {
try {
// Long click desired text
mDevice.waitForWindowUpdate(packageName, waitingTime)
mDevice.findObject(UiSelector().resourceId("$packageName:id/engineView"))
.waitForExists(waitingTime)
mDevice.findObject(UiSelector().textContains(expectedText)).waitForExists(waitingTime)
val link = mDevice.findObject(By.textContains(expectedText))
link.click(LONG_CLICK_DURATION)
// Click Select all from the text selection toolbar
if (selectAll) {
mDevice.findObject(UiSelector().textContains("Select all")).waitForExists(waitingTime)
val selectAllText = mDevice.findObject(By.textContains("Select all"))
selectAllText.click()
}
// Click Copy from the text selection toolbar
mDevice.findObject(UiSelector().textContains("Copy")).waitForExists(waitingTime)
val copyText = mDevice.findObject(By.textContains("Copy"))
copyText.click()
} catch (e: NullPointerException) {
println("Failed to long click desired text: ${e.localizedMessage}")
// Refresh the page in case the first long click didn't succeed
navigationToolbar {
}.openThreeDotMenu {
}.refreshPage {
mDevice.waitForIdle()
}
// Long click again the desired text
mDevice.waitForWindowUpdate(packageName, waitingTime)
mDevice.findObject(UiSelector().resourceId("$packageName:id/engineView"))
.waitForExists(waitingTime)
mDevice.findObject(UiSelector().textContains(expectedText)).waitForExists(waitingTime)
val link = mDevice.findObject(By.textContains(expectedText))
link.click(LONG_CLICK_DURATION)
// Click again Select all from the text selection toolbar
if (selectAll) {
mDevice.findObject(UiSelector().textContains("Select all")).waitForExists(waitingTime)
val selectAllText = mDevice.findObject(By.textContains("Select all"))
selectAllText.click()
}
// Click again Copy from the text selection toolbar
mDevice.findObject(UiSelector().textContains("Copy")).waitForExists(waitingTime)
val copyText = mDevice.findObject(By.textContains("Copy"))
copyText.click()
}
}
fun snackBarButtonClick(expectedText: String) {
onView(allOf(withId(R.id.snackbar_btn), withText(expectedText))).check(
matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))
@ -455,8 +509,10 @@ class BrowserRobot {
fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
mDevice.waitForIdle(waitingTime)
tabsCounter().click()
mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/tab_layout")),
waitingTime)
mDevice.waitNotNull(
Until.findObject(By.res("$packageName:id/tab_layout")),
waitingTime
)
TabDrawerRobot().interact()
return TabDrawerRobot.Transition()
@ -481,9 +537,9 @@ class BrowserRobot {
}
fun goToHomescreen(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
openTabDrawer {
}.openNewTab {
}.dismissSearchBar {}
onView(withContentDescription("Home screen"))
.check(matches(isDisplayed()))
.click()
HomeScreenRobot().interact()
return HomeScreenRobot.Transition()

@ -0,0 +1,275 @@
package org.mozilla.fenix.ui.robots
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.action.ViewActions.pressImeActionButton
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.swipeLeft
import androidx.test.espresso.action.ViewActions.swipeRight
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
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 org.hamcrest.Matchers.allOf
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull
class CollectionRobot {
fun verifySelectCollectionScreen() {
onView(withText("Select collection"))
.check(matches(isDisplayed()))
onView(withId(R.id.collections_list))
.check(matches(isDisplayed()))
onView(withText("Add new collection"))
.check(matches(isDisplayed()))
}
fun clickAddNewCollection() = addNewCollectionButton().click()
fun verifyCollectionNameTextField() {
mainMenuEditCollectionNameField().check(matches(isDisplayed()))
}
// names a collection saved from tab drawer
fun typeCollectionNameAndSave(collectionName: String) {
collectionNameTextField().perform(replaceText(collectionName))
mDevice.findObject(UiSelector().textContains("OK")).click()
}
fun verifyTabsSelectedCounterText(numOfTabs: Int) {
mDevice.findObject(UiSelector().text("Select tabs to save"))
.waitUntilGone(waitingTime)
val tabsCounter = onView(withId(R.id.bottom_bar_text))
when (numOfTabs) {
1 -> tabsCounter.check(matches(withText("$numOfTabs tab selected")))
2 -> tabsCounter.check(matches(withText("$numOfTabs tabs selected")))
}
}
fun saveTabsSelectedForCollection() {
onView(withId(R.id.save_button)).click()
}
fun verifyTabSavedInCollection(title: String, visible: Boolean = true) {
if (visible) {
scrollToElementByText(title)
collectionItem(title)
.check(
matches(isDisplayed())
)
} else
collectionItem(title)
.check(doesNotExist())
}
fun verifyCollectionTabUrl() {
onView(withId(R.id.caption)).check(matches(isDisplayed()))
}
fun verifyCollectionTabLogo() {
onView(withId(R.id.favicon)).check(matches(isDisplayed()))
}
fun verifyShareCollectionButtonIsVisible(visible: Boolean) {
shareCollectionButton()
.check(
if (visible) matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))
else matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))
)
}
fun clickShareCollectionButton() = onView(withId(R.id.collection_share_button)).click()
fun verifyCollectionMenuIsVisible(visible: Boolean) {
collectionThreeDotButton()
.check(
if (visible) matches(
withEffectiveVisibility(
ViewMatchers.Visibility.VISIBLE
)
)
else matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))
)
}
fun clickCollectionThreeDotButton() {
collectionThreeDotButton().click()
mDevice.waitNotNull(
Until.findObject(By.text("Delete collection")),
waitingTime
)
}
fun selectOpenTabs() {
onView(withText("Open tabs")).click()
}
fun selectRenameCollection() {
onView(withText("Rename collection")).click()
mDevice.waitNotNull(Until.findObject(By.text("Rename collection")))
}
fun selectAddTabToCollection() {
onView(withText("Add tab")).click()
mDevice.waitNotNull(Until.findObject(By.text("Select Tabs")))
}
fun selectDeleteCollection() {
onView(withText("Delete collection")).click()
mDevice.waitNotNull(Until.findObject(By.res("android:id/message")), waitingTime)
}
fun confirmDeleteCollection() {
onView(withText("DELETE")).click()
mDevice.waitNotNull(
Until.findObject(By.res("$packageName:id/no_collections_header")),
waitingTime
)
}
fun verifyCollectionItemRemoveButtonIsVisible(title: String, visible: Boolean) {
removeTabFromCollectionButton(title)
.check(
if (visible) matches(
withEffectiveVisibility(
ViewMatchers.Visibility.VISIBLE
)
)
else doesNotExist()
)
}
fun removeTabFromCollection(title: String) = removeTabFromCollectionButton(title).click()
fun swipeCollectionItemRight(title: String) {
scrollToElementByText(title)
// Swipping can sometimes fail to remove the tab, so if the tab still exists, we need to repeat it
var retries = 0 // number of retries before failing, will stop at 2
while (mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/label")
.text(title)
).exists() && retries < 2
) {
collectionItem(title).perform(swipeRight())
retries++
}
}
fun swipeCollectionItemLeft(title: String) {
scrollToElementByText(title)
// Swipping can sometimes fail to remove the tab, so if the tab still exists, we need to repeat it
var retries = 0 // number of retries before failing, will stop at 2
while (mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/label")
.text(title)
).exists() && retries < 2
) {
collectionItem(title).perform(swipeLeft())
retries++
}
}
fun verifySnackBarText(expectedText: String) {
mDevice.findObject(UiSelector().text(expectedText)).waitForExists(waitingTime)
}
fun goBackInCollectionFlow() = backButton().click()
class Transition {
fun collapseCollection(
title: String,
interact: HomeScreenRobot.() -> Unit
): HomeScreenRobot.Transition {
try {
mDevice.waitNotNull(Until.findObject(By.text(title)), waitingTime)
onView(allOf(withId(R.id.chevron), hasSibling(withText(title)))).click()
} catch (e: NoMatchingViewException) {
scrollToElementByText(title)
}
HomeScreenRobot().interact()
return HomeScreenRobot.Transition()
}
// names a collection saved from the 3dot menu
fun typeCollectionNameAndSave(
name: String,
interact: BrowserRobot.() -> Unit
): BrowserRobot.Transition {
mDevice.findObject(UiSelector().resourceId("$packageName:id/name_collection_edittext"))
.waitForExists(waitingTime)
mainMenuEditCollectionNameField().perform(
replaceText(name),
pressImeActionButton()
)
// wait for the collection creation wrapper to be dismissed
mDevice.waitNotNull(Until.gone(By.res("$packageName:id/createCollectionWrapper")))
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun selectExistingCollection(
title: String,
interact: BrowserRobot.() -> Unit
): BrowserRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.text(title)), waitingTime)
onView(withText(title)).click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
}
}
fun collectionRobot(interact: CollectionRobot.() -> Unit): CollectionRobot.Transition {
CollectionRobot().interact()
return CollectionRobot.Transition()
}
private fun collectionThreeDotButton() =
onView(withId(R.id.collection_overflow_button))
private fun collectionItem(title: String) =
onView(allOf(withId(R.id.label), withText(title)))
private fun shareCollectionButton() = onView(withId(R.id.collection_share_button))
private fun removeTabFromCollectionButton(title: String) =
onView(
allOf(
withId(R.id.secondary_button),
hasSibling(withText(title))
)
)
// collection name text field, opened from tab drawer
private fun collectionNameTextField() = onView(withId(R.id.collection_name))
// collection name text field, opened from main menu
private fun mainMenuEditCollectionNameField() =
onView(withId(R.id.name_collection_edittext))
private fun addNewCollectionButton() = onView(withText("Add new collection"))
private fun backButton() =
onView(withId(R.id.back_button))

@ -7,13 +7,14 @@ import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.uiautomator.UiSelector
import junit.framework.TestCase.assertTrue
import mozilla.components.support.ktx.android.content.appName
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.appName
/**
* Implementation of the robot pattern for Custom tabs
@ -29,7 +30,10 @@ class CustomTabRobot {
}
fun verifyPoweredByTextIsDisplayed() {
mDevice.findObject(UiSelector().textContains("POWERED BY ${appContext.appName}"))
assertTrue(
mDevice.findObject(UiSelector().textContains("POWERED BY $appName"))
.waitForExists(waitingTime)
)
}
fun verifyOpenInBrowserButtonExists() {
@ -43,7 +47,11 @@ class CustomTabRobot {
fun verifyRefreshButtonExists() = assertTrue(refreshButton().waitForExists(waitingTime))
fun verifyCustomMenuItem(label: String) {
assertTrue(mDevice.findObject(UiSelector().text(label)).exists())
assertTrue(mDevice.findObject(UiSelector().text(label)).waitForExists(waitingTime))
}
fun verifyCustomTabCloseButton() {
closeButton().check(matches(isDisplayed()))
}
class Transition {
@ -75,10 +83,12 @@ private fun desktopSiteButton() = onView(withId(R.id.switch_widget))
private fun findInPageButton() = onView(withText("Find in page"))
private fun openInBrowserButton() = onView(withText("Open in ${appContext.appName}"))
private fun openInBrowserButton() = onView(withText("Open in $appName"))
private fun refreshButton() = mDevice.findObject(UiSelector().description("Refresh"))
private fun forwardButton() = mDevice.findObject(UiSelector().description("Forward"))
private fun backButton() = mDevice.findObject(UiSelector().description("Back"))
private fun closeButton() = onView(withContentDescription("Return to previous app"))

@ -12,10 +12,10 @@ import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
@ -24,13 +24,13 @@ import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_APPS_PHOTOS
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_APPS_PHOTOS
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.packageName
/**
* Implementation of Robot Pattern for download UI handling.
@ -52,14 +52,16 @@ class DownloadRobot {
fun verifyDownloadedFileIcon() = assertDownloadedFileIcon()
fun verifyEmptyDownloadsList() {
mDevice.findObject(UiSelector().resourceId("org.mozilla.fenix.debug:id/download_empty_view"))
mDevice.findObject(UiSelector().resourceId("$packageName:id/download_empty_view"))
.waitForExists(waitingTime)
onView(withText("No downloaded files")).check(matches(isDisplayed()))
}
fun waitForDownloadsListToExist() =
assertTrue(mDevice.findObject(UiSelector().resourceId("org.mozilla.fenix.debug:id/download_list"))
.waitForExists(waitingTime))
assertTrue(
mDevice.findObject(UiSelector().resourceId("$packageName:id/download_list"))
.waitForExists(waitingTime)
)
class Transition {
fun clickDownload(interact: DownloadRobot.() -> Unit): Transition {

@ -179,8 +179,8 @@ private fun assertBasicLevelTrackingContentBlocked() {
withText(
containsString(
"social-track-digest256.dummytracker.org\n" +
"ads-track-digest256.dummytracker.org\n" +
"analytics-track-digest256.dummytracker.org"
"ads-track-digest256.dummytracker.org\n" +
"analytics-track-digest256.dummytracker.org"
)
)
)

@ -94,7 +94,7 @@ class HistoryRobot {
}
fun openThreeDotMenu(interact: ThreeDotMenuHistoryItemRobot.() -> Unit):
ThreeDotMenuHistoryItemRobot.Transition {
ThreeDotMenuHistoryItemRobot.Transition {
threeDotMenu().click()

@ -13,15 +13,11 @@ import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.swipeLeft
import androidx.test.espresso.action.ViewActions.swipeRight
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem
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.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withHint
@ -38,7 +34,6 @@ import androidx.test.uiautomator.Until
import androidx.test.uiautomator.Until.findObject
import junit.framework.TestCase.assertTrue
import mozilla.components.browser.state.state.searchEngines
import mozilla.components.support.ktx.android.content.appName
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.instanceOf
@ -48,6 +43,8 @@ import org.junit.Assert
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.appContext
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.click
@ -63,10 +60,10 @@ import org.mozilla.fenix.ui.util.STRING_ONBOARDING_TRACKING_PROTECTION_HEADER
*/
class HomeScreenRobot {
val privateSessionMessage =
"${appContext.appName} clears your search and browsing history from private tabs when you close them" +
" or quit the app. While this doesnt make you anonymous to websites or your internet" +
" service provider, it makes it easier to keep what you do online private from anyone" +
" else who uses this device."
"$appName clears your search and browsing history from private tabs when you close them" +
" or quit the app. While this doesnt make you anonymous to websites or your internet" +
" service provider, it makes it easier to keep what you do online private from anyone" +
" else who uses this device."
fun verifyNavigationToolbar() = assertNavigationToolbar()
fun verifyFocusedNavigationToolbar() = assertFocusedNavigationToolbar()
@ -119,127 +116,21 @@ class HomeScreenRobot {
fun verifyExistingTopSitesTabs(title: String) = assertExistingTopSitesTabs(title)
fun verifyTopSiteContextMenuItems() = assertTopSiteContextMenuItems()
// Collections element
fun clickCollectionThreeDotButton() {
collectionThreeDotButton().click()
mDevice.waitNotNull(findObject(text("Delete collection")), waitingTime)
}
fun selectOpenTabs() {
onView(allOf(withText("Open tabs"))).click()
}
fun selectRenameCollection() {
onView(allOf(withText("Rename collection"))).click()
mDevice.waitNotNull(findObject(text("Rename collection")))
}
fun selectAddTabToCollection() {
onView(allOf(withText("Add tab"))).click()
mDevice.waitNotNull(findObject(text("Select Tabs")))
}
fun selectDeleteCollection() {
onView(allOf(withText("Delete collection"))).click()
mDevice.waitNotNull(findObject(By.res("android:id/message")), waitingTime)
}
fun confirmDeleteCollection() {
onView(allOf(withText("DELETE"))).click()
mDevice.waitNotNull(
findObject(By.res("$packageName:id/no_collections_header")),
waitingTime
)
}
fun verifyCollectionIsDisplayed(title: String) {
mDevice.findObject(UiSelector().text(title)).waitForExists(waitingTime)
collectionTitle(title).check(matches(isDisplayed()))
}
fun verifyCollectionIcon() = onView(withId(R.id.collection_icon)).check(matches(isDisplayed()))
fun expandCollection(title: String) {
try {
mDevice.waitNotNull(findObject(text(title)), waitingTime)
collectionTitle(title).click()
} catch (e: NoMatchingViewException) {
scrollToElementByText(title)
}
}
fun collapseCollection(title: String) {
try {
mDevice.waitNotNull(findObject(text(title)), waitingTime)
onView(allOf(withId(R.id.chevron), hasSibling(withText(title)))).click()
} catch (e: NoMatchingViewException) {
// Collections elements
fun verifyCollectionIsDisplayed(title: String, collectionExists: Boolean = true) {
if (collectionExists) {
scrollToElementByText(title)
assertTrue(mDevice.findObject(UiSelector().text(title)).waitForExists(waitingTime))
} else {
scrollToElementByText("Collections")
assertTrue(mDevice.findObject(UiSelector().text(title)).waitUntilGone(waitingTime))
}
}
fun verifyTabSavedInCollection(title: String, visible: Boolean = true) {
try {
collectionItem(title)
.check(
if (visible) matches(isDisplayed()) else doesNotExist()
)
} catch (e: NoMatchingViewException) {
scrollToElementByText(title)
}
}
fun verifyCollectionTabLogo() =
onView(withId(R.id.favicon)).check(matches(isDisplayed()))
fun verifyCollectionTabUrl() =
onView(withId(R.id.caption)).check(matches(isDisplayed()))
fun verifyShareCollectionButtonIsVisible(visible: Boolean) {
shareCollectionButton()
.check(
if (visible) matches(withEffectiveVisibility(Visibility.VISIBLE))
else matches(withEffectiveVisibility(Visibility.GONE))
)
}
fun verifyCollectionMenuIsVisible(visible: Boolean) {
collectionThreeDotButton()
.check(
if (visible) matches(withEffectiveVisibility(Visibility.VISIBLE))
else matches(withEffectiveVisibility(Visibility.GONE))
)
}
fun verifyCollectionItemRemoveButtonIsVisible(title: String, visible: Boolean) {
removeTabFromCollectionButton(title)
.check(
if (visible) matches(withEffectiveVisibility(Visibility.VISIBLE))
else doesNotExist()
)
}
fun verifyCollectionIcon() = onView(withId(R.id.collection_icon)).check(matches(isDisplayed()))
fun verifyShareTabsOverlay() = assertShareTabsOverlay()
fun clickShareCollectionButton() = onView(withId(R.id.collection_share_button)).click()
fun removeTabFromCollection(title: String) = removeTabFromCollectionButton(title).click()
fun swipeCollectionItemRight(title: String) {
try {
collectionItem(title).perform(swipeRight())
} catch (e: NoMatchingViewException) {
scrollToElementByText(title)
}
}
fun swipeCollectionItemLeft(title: String) {
try {
collectionItem(title).perform(swipeLeft())
} catch (e: NoMatchingViewException) {
scrollToElementByText(title)
}
}
fun togglePrivateBrowsingModeOnOff() {
onView(ViewMatchers.withResourceName("privateBrowsingButton"))
.perform(click())
@ -316,7 +207,7 @@ class HomeScreenRobot {
}
fun triggerPrivateBrowsingShortcutPrompt(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenRobot.Transition {
// Loop to press the PB icon for 5 times to display the Add the Private Browsing Shortcut CFR
// Loop to press the PB icon for 5 times to display the Add the Private Browsing Shortcut CFR
for (i in 1..5) {
mDevice.findObject(UiSelector().resourceId("$packageName:id/privateBrowsingButton"))
.waitForExists(
@ -415,6 +306,19 @@ class HomeScreenRobot {
TabDrawerRobot().interact()
return TabDrawerRobot.Transition()
}
fun expandCollection(title: String, interact: CollectionRobot.() -> Unit): CollectionRobot.Transition {
try {
mDevice.waitNotNull(findObject(text(title)), waitingTime)
collectionTitle(title).click()
} catch (e: NoMatchingViewException) {
scrollToElementByText(title)
collectionTitle(title).click()
}
CollectionRobot().interact()
return CollectionRobot.Transition()
}
}
}
@ -424,7 +328,6 @@ fun homeScreen(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition
}
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
private fun homeScreenList() =
UiScrollable(
@ -453,7 +356,7 @@ private fun assertFocusedNavigationToolbar() =
private fun assertHomeScreen() {
mDevice.findObject(UiSelector().resourceId("$packageName:id/homeLayout")).waitForExists(waitingTime)
onView(ViewMatchers.withResourceName("homeLayout"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertHomeMenu() = onView(ViewMatchers.withResourceName("menuButton"))
@ -480,7 +383,8 @@ private fun assertCollectionsHeader() =
private fun assertNoCollectionsText() =
onView(
withText(
containsString("Collect the things that matter to you.\n" +
containsString(
"Collect the things that matter to you.\n" +
"Group together similar searches, sites, and tabs for quick access later."
)
)
@ -510,7 +414,7 @@ private fun verifySearchEngineIcon(searchEngineName: String) {
// First Run elements
private fun assertWelcomeHeader() =
onView(allOf(withText("Welcome to ${appContext.appName}!")))
onView(allOf(withText("Welcome to $appName!")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertStartSyncHeader() {
@ -597,7 +501,7 @@ private fun assertYourPrivacyText() {
onView(
allOf(
withText(
"Weve designed ${appContext.appName} to give you control over what you share online and what you share with us."
"Weve designed $appName to give you control over what you share online and what you share with us."
)
)
)
@ -637,9 +541,6 @@ private fun assertPrivateSessionMessage() =
onView(withId(R.id.private_session_description))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun collectionThreeDotButton() =
onView(allOf(withId(R.id.collection_overflow_button)))
private fun collectionTitle(title: String) =
onView(allOf(withId(R.id.collection_title), withText(title)))
@ -678,21 +579,8 @@ private fun assertShareTabsOverlay() {
private fun privateBrowsingButton() = onView(withId(R.id.privateBrowsingButton))
private fun collectionItem(title: String) =
onView(allOf(withId(R.id.label), withText(title)))
private fun saveTabsToCollectionButton() = onView(withId(R.id.add_tabs_to_collections_button))
private fun shareCollectionButton() = onView(withId(R.id.collection_share_button))
private fun removeTabFromCollectionButton(title: String) =
onView(
allOf(
withId(R.id.secondary_button),
hasSibling(withText(title))
)
)
private fun tabsCounter() = onView(withId(R.id.tab_button))
private fun startBrowsingButton(): UiObject {

@ -58,7 +58,8 @@ class LibrarySubMenusMultipleSelectionToolbarRobot {
mDevice.waitNotNull(
Until.findObject(
By.text("ALL ACTIONS")
), waitingTime
),
waitingTime
)
}
@ -68,7 +69,8 @@ class LibrarySubMenusMultipleSelectionToolbarRobot {
mDevice.waitNotNull(
Until.findObject(
By.text("ALL ACTIONS")
), waitingTime
),
waitingTime
)
}

@ -11,7 +11,6 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource
import androidx.test.espresso.IdlingResourceTimeoutException
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.pressImeActionButton
import androidx.test.espresso.action.ViewActions.replaceText
@ -33,6 +32,7 @@ import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import junit.framework.TestCase.assertTrue
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.anyOf
import org.hamcrest.CoreMatchers.containsString
@ -70,8 +70,10 @@ class NavigationToolbarRobot {
fun typeSearchTerm(searchTerm: String) = awesomeBar().perform(typeText(searchTerm))
fun toggleReaderView() {
mDevice.findObject(UiSelector()
.resourceId("$packageName:id/mozac_browser_toolbar_page_actions"))
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/mozac_browser_toolbar_page_actions")
)
.waitForExists(waitingTime)
readerViewToggle().click()
@ -116,43 +118,41 @@ class NavigationToolbarRobot {
return BrowserRobot.Transition()
}
fun openTrackingProtectionTestPage(url: Uri, etpEnabled: Boolean, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
sessionLoadedIdlingResource = SessionLoadedIdlingResource()
fun openTrackingProtectionTestPage(
url: Uri,
etpEnabled: Boolean,
interact: BrowserRobot.() -> Unit
): BrowserRobot.Transition {
openEditURLView()
awesomeBar().perform(replaceText(url.toString()), pressImeActionButton())
runWithIdleRes(sessionLoadedIdlingResource) {
when (etpEnabled) {
true ->
try {
onView(withId(R.id.onboarding_message))
.check(matches(isDisplayed()))
} catch (e: IdlingResourceTimeoutException) {
openThreeDotMenu {
}.stopPageLoad {
val onboardingDisplayed =
mDevice.findObject(UiSelector().resourceId("$packageName:id/onboarding_message"))
.waitForExists(waitingTime)
if (!onboardingDisplayed) {
openThreeDotMenu {
}.refreshPage {}
val onboardingMessage =
mDevice.findObject(UiSelector().resourceId("$packageName:id/onboarding_message"))
val onboardingDisplayed = onboardingMessage.waitForExists(waitingTime)
when (etpEnabled) {
true ->
try {
assertTrue(
"Onboarding message not displayed",
onboardingDisplayed
)
} catch (e: AssertionError) {
openThreeDotMenu {
}.stopPageLoad {
if (!onboardingDisplayed) {
openThreeDotMenu {
}.refreshPage {
assertTrue(onboardingDisplayed)
}
}
}
}
false ->
try {
onView(withResourceName("browserLayout")).check(matches(isDisplayed()))
} catch (e: IdlingResourceTimeoutException) {
openThreeDotMenu {
}.stopPageLoad {
}.openThreeDotMenu {
}.refreshPage {}
}
}
false ->
onView(withResourceName("browserLayout")).check(matches(isDisplayed()))
}
BrowserRobot().interact()
@ -187,8 +187,10 @@ class NavigationToolbarRobot {
fun openTabTray(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
mDevice.waitForIdle(waitingTime)
tabTrayButton().click()
mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/tab_layout")),
waitingTime)
mDevice.waitNotNull(
Until.findObject(By.res("$packageName:id/tab_layout")),
waitingTime
)
TabDrawerRobot().interact()
return TabDrawerRobot.Transition()
@ -254,7 +256,8 @@ class NavigationToolbarRobot {
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(
withText("Close tab")
), ViewActions.click()
),
ViewActions.click()
)
)
@ -270,7 +273,8 @@ class NavigationToolbarRobot {
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(
withText("New tab")
), ViewActions.click()
),
ViewActions.click()
)
)
@ -286,7 +290,8 @@ class NavigationToolbarRobot {
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(
withText("New private tab")
), ViewActions.click()
),
ViewActions.click()
)
)
@ -355,8 +360,10 @@ private fun readerViewToggle() =
onView(withParent(withId(R.id.mozac_browser_toolbar_page_actions)))
private fun assertReaderViewDetected(visible: Boolean) {
mDevice.findObject(UiSelector()
.description("Reader view"))
mDevice.findObject(
UiSelector()
.description("Reader view")
)
.waitForExists(waitingTime)
onView(
@ -371,8 +378,10 @@ private fun assertReaderViewDetected(visible: Boolean) {
}
private fun assertCloseReaderViewDetected(visible: Boolean) {
mDevice.findObject(UiSelector()
.description("Close reader view"))
mDevice.findObject(
UiSelector()
.description("Close reader view")
)
.waitForExists(waitingTime)
onView(

@ -55,9 +55,9 @@ class ReaderViewRobot {
val prefs = InstrumentationRegistry.getInstrumentation()
.targetContext.getSharedPreferences(
"mozac_feature_reader_view",
Context.MODE_PRIVATE
)
"mozac_feature_reader_view",
Context.MODE_PRIVATE
)
assertEquals(fontType, prefs.getString(fontTypeKey, ""))
}
@ -67,9 +67,9 @@ class ReaderViewRobot {
val prefs = InstrumentationRegistry.getInstrumentation()
.targetContext.getSharedPreferences(
"mozac_feature_reader_view",
Context.MODE_PRIVATE
)
"mozac_feature_reader_view",
Context.MODE_PRIVATE
)
val fontSizeKeyValue = prefs.getInt(fontSizeKey, 3)
@ -81,9 +81,9 @@ class ReaderViewRobot {
val prefs = InstrumentationRegistry.getInstrumentation()
.targetContext.getSharedPreferences(
"mozac_feature_reader_view",
Context.MODE_PRIVATE
)
"mozac_feature_reader_view",
Context.MODE_PRIVATE
)
assertEquals(expectedColorScheme, prefs.getString(colorSchemeKey, ""))
}

@ -18,6 +18,7 @@ import org.hamcrest.Matchers
import org.hamcrest.Matchers.allOf
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click
/**
@ -27,7 +28,7 @@ import org.mozilla.fenix.helpers.click
class RecentlyClosedTabsRobot {
fun waitForListToExist() =
mDevice.findObject(UiSelector().resourceId("org.mozilla.fenix.debug:id/recently_closed_list"))
mDevice.findObject(UiSelector().resourceId("$packageName:id/recently_closed_list"))
.waitForExists(
TestAssetHelper.waitingTime
)
@ -93,7 +94,8 @@ private fun assertRecentlyClosedTabsMenuView() {
)
)
.check(
matches(withEffectiveVisibility(Visibility.VISIBLE)))
matches(withEffectiveVisibility(Visibility.VISIBLE))
)
}
private fun assertEmptyRecentlyClosedTabsList() =
@ -104,7 +106,8 @@ private fun assertEmptyRecentlyClosedTabsList() =
)
)
.check(
matches(withText("No recently closed tabs here")))
matches(withText("No recently closed tabs here"))
)
private fun assertPageUrl(expectedUrl: Uri) = onView(
allOf(
@ -115,7 +118,8 @@ private fun assertPageUrl(expectedUrl: Uri) = onView(
)
)
.check(
matches(withText(Matchers.containsString(expectedUrl.toString()))))
matches(withText(Matchers.containsString(expectedUrl.toString())))
)
private fun recentlyClosedTabsPageTitle() = onView(
allOf(
@ -127,9 +131,11 @@ private fun recentlyClosedTabsPageTitle() = onView(
private fun assertRecentlyClosedTabsPageTitle(title: String) {
recentlyClosedTabsPageTitle()
.check(
matches(withEffectiveVisibility(Visibility.VISIBLE)))
matches(withEffectiveVisibility(Visibility.VISIBLE))
)
.check(
matches(withText(title)))
matches(withText(title))
)
}
private fun recentlyClosedTabsThreeDotButton() =
@ -137,45 +143,50 @@ private fun recentlyClosedTabsThreeDotButton() =
allOf(
withId(R.id.overflow_menu),
withEffectiveVisibility(
Visibility.VISIBLE
Visibility.VISIBLE
)
)
)
)
private fun assertRecentlyClosedTabsMenuCopy() =
onView(withText("Copy"))
.check(
matches(
withEffectiveVisibility(Visibility.VISIBLE)))
withEffectiveVisibility(Visibility.VISIBLE)
)
)
private fun assertRecentlyClosedTabsMenuShare() =
onView(withText("Share"))
.check(
matches(
withEffectiveVisibility(Visibility.VISIBLE)))
withEffectiveVisibility(Visibility.VISIBLE)
)
)
private fun assertRecentlyClosedTabsOverlayNewTab() =
onView(withText("Open in new tab"))
.check(
matches(
withEffectiveVisibility(Visibility.VISIBLE))
)
withEffectiveVisibility(Visibility.VISIBLE)
)
)
private fun assertRecentlyClosedTabsMenuPrivateTab() =
onView(withText("Open in private tab"))
.check(
matches(
withEffectiveVisibility(Visibility.VISIBLE)
)
)
)
private fun assertRecentlyClosedTabsMenuDelete() =
onView(withText("Delete"))
.check(
matches(
withEffectiveVisibility(Visibility.VISIBLE)
)
)
)
)
private fun recentlyClosedTabsCopyButton() = onView(withText("Copy"))
@ -184,26 +195,31 @@ private fun copySnackBarText() = onView(withId(R.id.snackbar_text))
private fun assertCopySnackBarText() = copySnackBarText()
.check(
matches
(withText("URL copied")))
(withText("URL copied"))
)
private fun recentlyClosedTabsShareButton() = onView(withText("Share"))
private fun assertRecentlyClosedShareOverlay() =
onView(withId(R.id.shareWrapper))
.check(
matches(ViewMatchers.isDisplayed()))
matches(ViewMatchers.isDisplayed())
)
private fun assetRecentlyClosedShareTitle(title: String) =
onView(withId(R.id.share_tab_title))
.check(
matches(ViewMatchers.isDisplayed()))
matches(ViewMatchers.isDisplayed())
)
.check(
matches(withText(title)))
matches(withText(title))
)
private fun assertRecentlyClosedShareFavicon() =
onView(withId(R.id.share_tab_favicon))
.check(
matches(ViewMatchers.isDisplayed()))
matches(ViewMatchers.isDisplayed())
)
private fun assertRecentlyClosedShareUrl(expectedUrl: Uri) =
onView(
@ -213,7 +229,8 @@ private fun assertRecentlyClosedShareUrl(expectedUrl: Uri) =
)
)
.check(
matches(withText(Matchers.containsString(expectedUrl.toString()))))
matches(withText(Matchers.containsString(expectedUrl.toString())))
)
private fun recentlyClosedTabsNewTabButton() = onView(withText("Open in new tab"))

@ -22,6 +22,7 @@ import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withSubstring
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
@ -35,6 +36,7 @@ import org.hamcrest.CoreMatchers.startsWith
import org.hamcrest.Matchers
import org.junit.Assert.assertEquals
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION
import org.mozilla.fenix.helpers.SessionLoadedIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
@ -71,8 +73,10 @@ class SearchRobot {
selectDefaultSearchEngine(searchEngineName)
fun clickSearchEngineShortcutButton() {
val searchEnginesShortcutButton = mDevice.findObject(UiSelector()
.resourceId("$packageName:id/search_engines_shortcut_button"))
val searchEnginesShortcutButton = mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/search_engines_shortcut_button")
)
searchEnginesShortcutButton.waitForExists(waitingTime)
searchEnginesShortcutButton.click()
}
@ -126,6 +130,24 @@ class SearchRobot {
clearButton().perform(click())
}
fun longClickToolbar() {
mDevice.waitForWindowUpdate(packageName, waitingTime)
mDevice.findObject(UiSelector().resourceId("$packageName:id/awesomeBar"))
.waitForExists(waitingTime)
mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
.waitForExists(waitingTime)
val toolbar = mDevice.findObject(By.res("$packageName:id/toolbar"))
toolbar.click(LONG_CLICK_DURATION)
}
fun clickPasteText() {
mDevice.findObject(UiSelector().textContains("Paste")).waitForExists(waitingTime)
val pasteText = mDevice.findObject(By.textContains("Paste"))
pasteText.click()
}
fun verifyPastedToolbarText(expectedText: String) = assertPastedToolbarText(expectedText)
class Transition {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource
@ -202,10 +224,10 @@ private fun searchWrapper() = onView(withId(R.id.search_wrapper))
private fun assertSearchEngineURL(searchEngineName: String) {
mDevice.waitNotNull(
Until.findObject(By.textContains("${searchEngineName.toLowerCase()}.com/?q=mozilla")),
Until.findObject(By.textContains("${searchEngineName.lowercase()}.com/?q=mozilla")),
TestAssetHelper.waitingTime
)
onView(allOf(withText(startsWith("${searchEngineName.toLowerCase()}.com"))))
onView(allOf(withText(startsWith("${searchEngineName.lowercase()}.com"))))
.check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
@ -250,7 +272,8 @@ private fun assertKeyboardVisibility(isExpectedToBeVisible: Boolean) = {
mDevice.waitNotNull(
Until.findObject(
By.text("Search Engine")
), waitingTime
),
waitingTime
)
assertEquals(
isExpectedToBeVisible,
@ -295,4 +318,17 @@ private fun assertDefaultSearchEngine(expectedText: String) {
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertPastedToolbarText(expectedText: String) {
mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
.waitForExists(waitingTime)
mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_url_view"))
.waitForExists(waitingTime)
onView(
allOf(
withSubstring(expectedText),
withId(R.id.mozac_browser_toolbar_edit_url_view)
)
).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
private fun goBackButton() = onView(allOf(withContentDescription("Navigate up")))

@ -33,6 +33,7 @@ import org.hamcrest.CoreMatchers
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_PLAY_SERVICES
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.assertIsEnabled
import org.mozilla.fenix.helpers.click
@ -110,7 +111,7 @@ class SettingsRobot {
}
fun openAboutFirefoxPreview(interact: SettingsSubMenuAboutRobot.() -> Unit):
SettingsSubMenuAboutRobot.Transition {
SettingsSubMenuAboutRobot.Transition {
assertAboutFirefoxPreview().click()
@ -119,7 +120,7 @@ class SettingsRobot {
}
fun openSearchSubMenu(interact: SettingsSubMenuSearchRobot.() -> Unit):
SettingsSubMenuSearchRobot.Transition {
SettingsSubMenuSearchRobot.Transition {
fun searchEngineButton() = onView(withText("Search"))
searchEngineButton().click()
@ -147,15 +148,19 @@ class SettingsRobot {
}
fun openAccessibilitySubMenu(interact: SettingsSubMenuAccessibilityRobot.() -> Unit): SettingsSubMenuAccessibilityRobot.Transition {
scrollToElementByText("Accessibility")
fun accessibilityButton() = onView(withText("Accessibility"))
accessibilityButton().click()
accessibilityButton()
.check(matches(isDisplayed()))
.click()
SettingsSubMenuAccessibilityRobot().interact()
return SettingsSubMenuAccessibilityRobot.Transition()
}
fun openLanguageSubMenu(interact: SettingsSubMenuLanguageRobot.() -> Unit): SettingsSubMenuLanguageRobot.Transition {
scrollToElementByText("Language")
fun languageButton() = onView(withText("Language"))
languageButton().click()
@ -310,14 +315,8 @@ private fun assertDefaultBrowserIsDisabled() {
}
private fun toggleDefaultBrowserSwitch() {
scrollToElementByText("Set as default browser")
onView(
CoreMatchers.allOf(
ViewMatchers.withParent(CoreMatchers.not(withId(R.id.navigationToolbar))),
withText("Set as default browser")
)
)
.perform(ViewActions.click())
scrollToElementByText("Privacy and security")
onView(withText("Set as default browser")).perform(ViewActions.click())
}
private fun assertAndroidDefaultAppsMenuAppears() {
@ -402,7 +401,7 @@ private fun assertNotificationsButton() {
private fun assertDataCollectionButton() {
scrollToElementByText("Data collection")
onView(withText("Data collection"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun openLinksInAppsButton() = onView(withText("Open links in apps"))
@ -475,8 +474,8 @@ private fun assertRateOnGooglePlay(): ViewInteraction {
private fun assertAboutFirefoxPreview(): ViewInteraction {
onView(withId(R.id.recycler_view))
.perform(RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(hasDescendant(withText("About Firefox Preview"))))
return onView(withText("About Firefox Preview"))
.perform(RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(hasDescendant(withText("About $appName"))))
return onView(withText("About $appName"))
.check(matches(isDisplayed()))
}

@ -28,6 +28,7 @@ import org.hamcrest.CoreMatchers.containsString
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.isVisibleForUser
import org.mozilla.fenix.settings.SupportUtils
import java.text.SimpleDateFormat
@ -108,7 +109,7 @@ private fun assertVersionNumber() {
private fun assertProductCompany() {
onView(withId(R.id.about_content))
.check(matches(withText(containsString("Firefox Preview is produced by Mozilla."))))
.check(matches(withText(containsString("$appName is produced by Mozilla."))))
}
private fun assertCurrentTimestamp() {
@ -116,17 +117,17 @@ private fun assertCurrentTimestamp() {
// Currently UI tests run against debug builds, which display a hard-coded string 'debug build'
// instead of the date. See https://github.com/mozilla-mobile/fenix/pull/10812#issuecomment-633746833
.check(matches(withText(containsString("debug build"))))
// This assertion should be valid for non-debug build types.
// .check(BuildDateAssertion.isDisplayedDateAccurate())
// This assertion should be valid for non-debug build types.
// .check(BuildDateAssertion.isDisplayedDateAccurate())
}
private fun assertWhatIsNewInFirefoxPreview() {
if (!onView(withText("Whats new in Firefox Preview")).isVisibleForUser()) {
if (!onView(withText("Whats new in $appName")).isVisibleForUser()) {
onView(withId(R.id.about_layout)).perform(ViewActions.swipeUp())
}
onView(withText("Whats new in Firefox Preview"))
onView(withText("Whats new in $appName"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.perform(click())
@ -222,7 +223,7 @@ private fun assertLibrariesUsed() {
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.perform(click())
onView(withId(R.id.navigationToolbar)).check(matches(hasDescendant(withText(containsString("Firefox Preview | OSS Libraries")))))
onView(withId(R.id.navigationToolbar)).check(matches(hasDescendant(withText(containsString("$appName | OSS Libraries")))))
Espresso.pressBack()
}

@ -2,31 +2,35 @@ package org.mozilla.fenix.ui.robots
import android.widget.RelativeLayout
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingResourceTimeoutException
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
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.isAssignableFrom
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withParent
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.rule.ActivityTestRule
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.instanceOf
import org.hamcrest.CoreMatchers.not
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.IdlingResourceHelper.registerAddonInstallingIdlingResource
import org.mozilla.fenix.helpers.IdlingResourceHelper.unregisterAddonInstallingIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull
@ -36,9 +40,23 @@ import org.mozilla.fenix.helpers.ext.waitNotNull
class SettingsSubMenuAddonsManagerRobot {
fun verifyAddonPrompt(addonName: String) = assertAddonPrompt(addonName)
fun clickInstallAddon(addonName: String) = selectInstallAddon(addonName)
fun verifyDownloadAddonPrompt(addonName: String, activityTestRule: HomeActivityTestRule) =
assertDownloadingAddonPrompt(addonName, activityTestRule)
fun verifyDownloadAddonPrompt(
addonName: String,
activityTestRule: ActivityTestRule<HomeActivity>
) {
try {
assertDownloadingAddonPrompt(addonName, activityTestRule)
} catch (e: IdlingResourceTimeoutException) {
if (mDevice.findObject(UiSelector().text("Failed to install $addonName")).exists()) {
clickInstallAddon(addonName)
acceptInstallAddon()
assertDownloadingAddonPrompt(addonName, activityTestRule)
}
}
}
fun cancelInstallAddon() = cancelInstall()
fun acceptInstallAddon() = allowInstall()
@ -124,7 +142,7 @@ class SettingsSubMenuAddonsManagerRobot {
private fun assertDownloadingAddonPrompt(
addonName: String,
activityTestRule: HomeActivityTestRule
activityTestRule: ActivityTestRule<HomeActivity>
) {
registerAddonInstallingIdlingResource(activityTestRule)
@ -132,7 +150,7 @@ class SettingsSubMenuAddonsManagerRobot {
allOf(
withText("Okay, Got it"),
withParent(instanceOf(RelativeLayout::class.java)),
hasSibling(withText("$addonName has been added to Firefox Preview")),
hasSibling(withText("$addonName has been added to $appName")),
hasSibling(withText("Open it in the menu")),
hasSibling(withText("Allow in private browsing"))
)

@ -16,6 +16,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import org.hamcrest.CoreMatchers.allOf
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.assertIsEnabled
import org.mozilla.fenix.helpers.click
@ -58,8 +59,11 @@ private fun goBackButton() =
onView(withContentDescription("Navigate up"))
private fun assertNavigationToolBarHeader() = onView(
allOf(withParent(withId(R.id.navigationToolbar)),
withText(R.string.preferences_data_collection)))
allOf(
withParent(withId(R.id.navigationToolbar)),
withText(R.string.preferences_data_collection)
)
)
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertDataCollectionOptions() {
@ -68,7 +72,7 @@ private fun assertDataCollectionOptions() {
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
val usageAndTechnicalDataText =
"Shares performance, usage, hardware and customization data about your browser with Mozilla to help us make Firefox Preview better"
"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)))

@ -62,18 +62,27 @@ class SettingsSubMenuDeleteBrowsingDataOnQuitRobot {
private fun goBackButton() = onView(withContentDescription("Navigate up"))
private fun assertNavigationToolBarHeader() = onView(allOf(withId(R.id.navigationToolbar),
withChild(withText(R.string.preferences_delete_browsing_data_on_quit))))
private fun assertNavigationToolBarHeader() = onView(
allOf(
withId(R.id.navigationToolbar),
withChild(withText(R.string.preferences_delete_browsing_data_on_quit))
)
)
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun deleteBrowsingOnQuitButton() = onView(allOf(withParentIndex(0),
withChild(withText(R.string.preferences_delete_browsing_data_on_quit))))
private fun deleteBrowsingOnQuitButton() = onView(
allOf(
withParentIndex(0),
withChild(withText(R.string.preferences_delete_browsing_data_on_quit))
)
)
private fun assertDeleteBrowsingOnQuitButton() = deleteBrowsingOnQuitButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertDeleteBrowsingOnQuitButtonSummary() = onView(
withText(R.string.preference_summary_delete_browsing_data_on_quit_2))
withText(R.string.preference_summary_delete_browsing_data_on_quit_2)
)
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertDeleteBrowsingOnQuitButtonSwitchDefault() = onView(withResourceName("switch_widget"))

@ -21,9 +21,10 @@ import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers.allOf
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.assertIsChecked
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.TestAssetHelper
/**
* Implementation of Robot Pattern for the settings Delete Browsing Data sub menu.
@ -85,8 +86,12 @@ private fun goBackButton() =
onView(allOf(withContentDescription("Navigate up")))
private fun assertNavigationToolBarHeader() {
onView(allOf(withId(R.id.navigationToolbar),
withChild(withText(R.string.preferences_delete_browsing_data))))
onView(
allOf(
withId(R.id.navigationToolbar),
withChild(withText(R.string.preferences_delete_browsing_data))
)
)
.check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
}
@ -104,7 +109,7 @@ private fun cancelButton() =
mDevice.findObject(UiSelector().textStartsWith("CANCEL"))
private fun assertMessageInDialogBox() =
onView(withText("Firefox Preview will delete the selected browsing data."))
onView(withText("$appName will delete the selected browsing data."))
.inRoot(isDialog())
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))

@ -10,9 +10,9 @@ import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBack
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.Visibility
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withChild
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
@ -26,6 +26,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.assertIsChecked
import org.mozilla.fenix.helpers.click
@ -110,8 +111,12 @@ class SettingsSubMenuEnhancedTrackingProtectionRobot {
}
private fun assertNavigationToolBarHeader() {
onView(allOf(withParent(withId(org.mozilla.fenix.R.id.navigationToolbar)),
withText("Enhanced Tracking Protection")))
onView(
allOf(
withParent(withId(org.mozilla.fenix.R.id.navigationToolbar)),
withText("Enhanced Tracking Protection")
)
)
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
@ -121,21 +126,32 @@ private fun assertEnhancedTrackingProtectionHeader() {
}
private fun assertEnhancedTrackingProtectionHeaderDescription() {
onView(allOf(withParent(withParentIndex(0)),
withText("Keep your data to yourself. Firefox Preview protects you from many of the most common trackers that follow what you do online.")))
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.")
)
)
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertLearnMoreText() {
onView(allOf(withParent(withParentIndex(0)),
withText("Learn more")))
onView(
allOf(
withParent(withParentIndex(0)),
withText("Learn more")
)
)
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertEnhancedTrackingProtectionTextWithSwitchWidget() {
onView(allOf(
onView(
allOf(
withParentIndex(1),
withChild(withText("Enhanced Tracking Protection"))))
withChild(withText("Enhanced Tracking Protection"))
)
)
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
@ -194,7 +210,8 @@ private fun assertTrackingProtectionSwitchEnabled() {
}
private fun assertRadioButtonDefaults() {
onView(withText("Strict")
onView(
withText("Strict")
).assertIsChecked(false)
onView(
@ -204,7 +221,8 @@ private fun assertRadioButtonDefaults() {
)
).assertIsChecked(true)
onView(withText("Custom")
onView(
withText("Custom")
).assertIsChecked(false)
}

@ -9,9 +9,24 @@ import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import junit.framework.TestCase.assertTrue
import org.hamcrest.CoreMatchers
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.packageName
class SettingsSubMenuLanguageRobot {
fun selectLanguage(language: String) {
languagesList.waitForExists(waitingTime)
languagesList
.getChildByText(UiSelector().text(language), language)
.click()
}
fun verifyLanguageHeaderIsTranslated(translation: String) =
assertTrue(mDevice.findObject(UiSelector().text(translation)).waitForExists(waitingTime))
class Transition {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@ -27,3 +42,10 @@ class SettingsSubMenuLanguageRobot {
private fun goBackButton() =
onView(CoreMatchers.allOf(ViewMatchers.withContentDescription("Navigate up")))
private val languagesList =
UiScrollable(
UiSelector()
.resourceId("$packageName:id/locale_list")
.scrollable(true)
)

@ -18,10 +18,10 @@ import org.hamcrest.CoreMatchers
class SettingsSubMenuLoginsAndPasswordOptionsToSaveRobot {
fun verifySaveLoginsOptionsView() {
onView(ViewMatchers.withText("Ask to save"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
onView(ViewMatchers.withText("Never save"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
class Transition {
@ -35,4 +35,4 @@ class SettingsSubMenuLoginsAndPasswordOptionsToSaveRobot {
}
private fun goBackButton() =
onView(CoreMatchers.allOf(ViewMatchers.withContentDescription("Navigate up")))
onView(CoreMatchers.allOf(ViewMatchers.withContentDescription("Navigate up")))

@ -6,6 +6,7 @@
package org.mozilla.fenix.ui.robots
import android.content.Context
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions.matches
@ -16,6 +17,7 @@ import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull
@ -40,7 +42,7 @@ class SettingsSubMenuLoginsAndPasswordRobot {
fun verifyDefaultValueExceptions() = assertDefaultValueExceptions()
fun verifyDefaultValueAutofillLogins() = assertDefaultValueAutofillLogins()
fun verifyDefaultValueAutofillLogins(context: Context) = assertDefaultValueAutofillLogins(context)
class Transition {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@ -92,16 +94,23 @@ fun settingsSubMenuLoginsAndPassword(interact: SettingsSubMenuLoginsAndPasswordR
}
private fun goBackButton() =
onView(CoreMatchers.allOf(ViewMatchers.withContentDescription("Navigate up")))
onView(CoreMatchers.allOf(ViewMatchers.withContentDescription("Navigate up")))
private fun assertDefaultView() = onView(ViewMatchers.withText("Sync logins across devices"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun assertDefaultValueAutofillLogins() = onView(ViewMatchers.withText("Autofill"))
private fun assertDefaultValueAutofillLogins(context: Context) = onView(
ViewMatchers.withText(
context.getString(
R.string.preferences_passwords_autofill2,
context.getString(R.string.app_name)
)
)
)
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun assertDefaultValueExceptions() = onView(ViewMatchers.withText("Exceptions"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun assertDefaultValueSyncLogins() = onView(ViewMatchers.withText("Sign in to Sync"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))

@ -25,6 +25,7 @@ import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.isEnabled
@ -104,7 +105,7 @@ private fun goBackButton() = onView(withContentDescription("Navigate up"))
private fun addAutomaticallyButton() =
mDevice.findObject(UiSelector().textStartsWith("add automatically"))
private fun privateBrowsingShortcutIcon() = mDevice.findObject(text("Private Firefox Preview"))
private fun privateBrowsingShortcutIcon() = mDevice.findObject(text("Private $appName"))
private fun assertAddPrivateBrowsingShortcutButton() {
mDevice.wait(
@ -130,6 +131,6 @@ private fun assertOpenLinksInPrivateTabOff() {
}
private fun assertPrivateBrowsingShortcutIcon() {
mDevice.wait(Until.findObject(text("Private Firefox Preview")), waitingTime)
assertTrue(mDevice.hasObject(text("Private Firefox Preview")))
mDevice.wait(Until.findObject(text("Private $appName")), waitingTime)
assertTrue(mDevice.hasObject(text("Private $appName")))
}

@ -24,6 +24,7 @@ import androidx.test.uiautomator.UiSelector
import org.hamcrest.CoreMatchers
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click
/**
@ -53,7 +54,7 @@ class SettingsSubMenuSearchRobot {
fun saveNewSearchEngine() {
addSearchEngineSaveButton().click()
mDevice.findObject(
UiSelector().resourceId("org.mozilla.fenix.debug:id/recycler_view")
UiSelector().resourceId("$packageName:id/recycler_view")
).waitForExists(waitingTime)
}

@ -139,11 +139,11 @@ private fun assertCheckAutoPayRadioButtonDefault() {
// Block audio only
onView(withId(R.id.third_radio))
.assertIsChecked(isChecked = false)
.assertIsChecked(isChecked = true)
// Block audio and video
onView(withId(R.id.fourth_radio))
.assertIsChecked(isChecked = true)
.assertIsChecked(isChecked = false)
}
private fun assertAskToAllowRecommended() = onView(withId(R.id.ask_to_allow_radio))

@ -159,7 +159,7 @@ class SettingsSubMenuSitePermissionsRobot {
onView(withText("Autoplay"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
val autoplayText = "Block audio and video"
val autoplayText = "Block audio only"
onView(withText(autoplayText))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))

@ -9,11 +9,14 @@ package org.mozilla.fenix.ui.robots
import androidx.test.espresso.Espresso.onView
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.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import org.hamcrest.CoreMatchers.allOf
import org.mozilla.fenix.helpers.click
/**
* Implementation of Robot Pattern for the settings Tabs sub menu.
@ -22,6 +25,10 @@ class SettingsSubMenuTabsRobot {
fun verifyOptions() = assertOptions()
fun verifyStartOnHomeOptions() = assertStartOnHomeOptions()
fun clickAlwaysStartOnHomeToggle() = alwaysStartOnHomeToggle().click()
class Transition {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@ -46,6 +53,17 @@ private fun assertOptions() {
.check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
private fun assertStartOnHomeOptions() {
startOnHomeHeading()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
afterFourHoursToggle()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
alwaysStartOnHomeToggle()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
neverStartOnHomeToggle()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
private fun manualToggle() = onView(withText("Manually"))
private fun afterOneDayToggle() = onView(withText("After one day"))
@ -54,5 +72,13 @@ private fun afterOneWeekToggle() = onView(withText("After one week"))
private fun afterOneMonthToggle() = onView(withText("After one month"))
private fun startOnHomeHeading() = onView(withText("Start on home"))
private fun afterFourHoursToggle() = onView(withText("After four hours"))
private fun alwaysStartOnHomeToggle() = onView(withText("Always"))
private fun neverStartOnHomeToggle() = onView(withText("Never"))
private fun goBackButton() =
onView(allOf(ViewMatchers.withContentDescription("Navigate up")))

@ -36,6 +36,10 @@ class SettingsSubMenuThemeRobot {
fun selectLightMode() = lightModeToggle().click()
fun clickTopToolbarToggle() = topToolbarToggle().click()
fun clickBottomToolbarToggle() = bottomToolbarToggle().click()
class Transition {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@ -62,6 +66,10 @@ private fun darkModeToggle() = onView(withText("Dark"))
private fun lightModeToggle() = onView(withText("Light"))
private fun topToolbarToggle() = onView(withText("Top"))
private fun bottomToolbarToggle() = onView(withText("Bottom"))
private fun deviceModeToggle(): ViewInteraction {
val followDeviceThemeText =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) "Follow device theme" else "Set by Battery Saver"

@ -46,13 +46,13 @@ class SettingsTurnOnSyncRobot {
}
private fun goBackButton() =
Espresso.onView(CoreMatchers.allOf(ViewMatchers.withContentDescription("Navigate up")))
Espresso.onView(CoreMatchers.allOf(ViewMatchers.withContentDescription("Navigate up")))
private fun assertUseEmailField() = Espresso.onView(ViewMatchers.withText("Use email instead"))
.check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun assertReadyToScan() = Espresso.onView(ViewMatchers.withText("Ready to scan"))
.check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun useEmailButton() = Espresso.onView(ViewMatchers.withText("Use email instead"))

@ -1,46 +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.robots
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import org.hamcrest.CoreMatchers.allOf
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.click
/**
* Implementation of Robot Pattern for Synced Tabs sub menu.
*/
class SyncedTabsRobot {
fun verifySyncedTabsMenuHeader() = assertSyncedTabsMenuHeader()
class Transition {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())!!
fun goBack(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
goBackButton().click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
}
}
private fun goBackButton() =
onView(allOf(withContentDescription("Navigate up")))
private fun assertSyncedTabsMenuHeader() {
// Replaced with the new string here, the test is assuming we are NOT signed in
// Sync tests in SettingsSyncTest are still TO-DO, so I'm not sure that we have a test for signing into Sync
onView(withText(R.string.sync_menu_sign_in))
.check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
}

@ -39,7 +39,8 @@ fun systemSettings(interact: SystemSettingsRobot.() -> Unit): SystemSettingsRobo
private fun assertSystemNotificationsView() {
mDevice.findObject(UiSelector().resourceId("com.android.settings:id/list"))
.waitForExists(waitingTime)
assertTrue(mDevice.findObject(UiSelector().textContains("Show notifications"))
.waitForExists(waitingTime)
assertTrue(
mDevice.findObject(UiSelector().textContains("Show notifications"))
.waitForExists(waitingTime)
)
}

@ -18,7 +18,7 @@ import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.GeneralLocation
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.longClick
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
@ -36,6 +36,7 @@ import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import androidx.test.uiautomator.Until.findObject
import com.google.android.material.bottomsheet.BottomSheetBehavior
import junit.framework.TestCase.assertTrue
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.anyOf
import org.hamcrest.CoreMatchers.containsString
@ -60,7 +61,7 @@ class TabDrawerRobot {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mDevice.waitNotNull(
Until.findObject(By.res("org.mozilla.fenix.debug:id/mozac_browser_tabstray_url")),
Until.findObject(By.res("$packageName:id/mozac_browser_tabstray_url")),
waitingTime
)
onView(withId(R.id.mozac_browser_tabstray_url))
@ -88,7 +89,7 @@ class TabDrawerRobot {
fun closeTab() {
mDevice.findObject(
UiSelector().resourceId("org.mozilla.fenix.debug:id/mozac_browser_tabstray_close")
UiSelector().resourceId("$packageName:id/mozac_browser_tabstray_close")
).waitForExists(waitingTime)
var retries = 0 // number of retries before failing, will stop at 2
@ -96,7 +97,7 @@ class TabDrawerRobot {
closeTabButton().click()
retries++
} while (mDevice.findObject(
UiSelector().resourceId("org.mozilla.fenix.debug:id/mozac_browser_tabstray_close")
UiSelector().resourceId("$packageName:id/mozac_browser_tabstray_close")
).exists() && retries < 3
)
}
@ -150,7 +151,7 @@ class TabDrawerRobot {
mDevice.waitNotNull(
findObject(
By
.res("org.mozilla.fenix.debug:id/play_pause_button")
.res("$packageName:id/play_pause_button")
.desc(action)
),
waitingTime
@ -173,8 +174,6 @@ class TabDrawerRobot {
selectTabsButton.click()
}
fun clickAddNewCollection() = addNewCollectionButton().click()
fun selectTab(title: String) {
mDevice.waitNotNull(
findObject(text(title)),
@ -185,11 +184,14 @@ class TabDrawerRobot {
tab.click()
}
fun clickSaveCollection() = saveTabsToCollectionButton().click()
fun longClickTab(title: String) {
mDevice.waitNotNull(
findObject(text(title)),
waitingTime
)
fun typeCollectionName(collectionName: String) {
collectionNameTextField().perform(replaceText(collectionName))
mDevice.findObject(UiSelector().textContains("OK")).click()
val tab = onView(withText(title))
tab.perform(longClick())
}
fun createCollection(
@ -199,10 +201,19 @@ class TabDrawerRobot {
) {
clickSelectTabs()
selectTab(tabTitle)
clickSaveCollection()
if (!firstCollection)
clickAddNewCollection()
typeCollectionName(collectionName)
tabDrawer {
}.clickSaveCollection {
if (!firstCollection)
clickAddNewCollection()
typeCollectionNameAndSave(collectionName)
}
}
fun verifyTabsMultiSelectionCounter(numOfTabs: Int) {
assertTrue(
mDevice.findObject(UiSelector().text("$numOfTabs selected"))
.waitForExists(waitingTime)
)
}
class Transition {
@ -219,8 +230,10 @@ class TabDrawerRobot {
fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
mDevice.waitForIdle(waitingTime)
tabsCounter().click()
mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/tab_layout")),
waitingTime)
mDevice.waitNotNull(
Until.findObject(By.res("$packageName:id/tab_layout")),
waitingTime
)
TabDrawerRobot().interact()
return TabDrawerRobot.Transition()
@ -327,7 +340,7 @@ class TabDrawerRobot {
}
fun openRecentlyClosedTabs(interact: RecentlyClosedTabsRobot.() -> Unit):
RecentlyClosedTabsRobot.Transition {
RecentlyClosedTabsRobot.Transition {
threeDotMenu().click()
@ -343,6 +356,14 @@ class TabDrawerRobot {
RecentlyClosedTabsRobot().interact()
return RecentlyClosedTabsRobot.Transition()
}
fun clickSaveCollection(interact: CollectionRobot.() -> Unit):
CollectionRobot.Transition {
saveTabsToCollectionButton().click()
CollectionRobot().interact()
return CollectionRobot.Transition()
}
}
}
@ -376,9 +397,11 @@ private fun threeDotMenu() = onView(withId(R.id.tab_tray_overflow))
private fun assertExistingOpenTabs(title: String) {
try {
mDevice.findObject(UiSelector()
.resourceId("$packageName:id/mozac_browser_tabstray_title")
.textContains(title))
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/mozac_browser_tabstray_title")
.textContains(title)
)
.waitForExists(waitingTime)
tab(title).check(matches(isDisplayed()))
@ -464,8 +487,4 @@ private fun tabsCounter() = onView(withId(R.id.tab_button))
private fun visibleOrGone(visibility: Boolean) =
if (visibility) ViewMatchers.Visibility.VISIBLE else ViewMatchers.Visibility.GONE
private fun addNewCollectionButton() = onView(withId(R.id.add_new_collection))
private fun saveTabsToCollectionButton() = onView(withId(R.id.collect_multi_select))
private fun collectionNameTextField() = onView(withId(R.id.collection_name))

@ -34,7 +34,8 @@ class ThreeDotMenuHistoryItemRobot {
mDevice.waitNotNull(
Until.findObject(
By.text("ALL ACTIONS")
), TestAssetHelper.waitingTime
),
TestAssetHelper.waitingTime
)
LibrarySubMenusMultipleSelectionToolbarRobot().interact()

@ -8,7 +8,6 @@ package org.mozilla.fenix.ui.robots
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.swipeDown
import androidx.test.espresso.action.ViewActions.swipeUp
import androidx.test.espresso.assertion.ViewAssertions.matches
@ -33,6 +32,7 @@ import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.share.ShareFragment
@ -50,7 +50,6 @@ class ThreeDotMenuMainRobot {
fun verifyAddOnsButton() = assertAddOnsButton()
fun verifyHistoryButton() = assertHistoryButton()
fun verifyBookmarksButton() = assertBookmarksButton()
fun verifySyncedTabsButton() = assertSyncedTabsButton()
fun verifySyncSignInButton() = assertSignInToSyncButton()
fun verifyHelpButton() = assertHelpButton()
fun verifyThreeDotMenuExists() = threeDotMenuRecyclerViewExists()
@ -80,15 +79,6 @@ class ThreeDotMenuMainRobot {
fun verifySaveCollection() = assertSaveCollectionButton()
fun verifySelectTabs() = assertSelectTabsButton()
fun clickSaveCollectionButton() {
browserViewSaveCollectionButton().click()
}
fun clickAddNewCollection() {
addNewCollectionButton().click()
}
fun verifyCollectionNameTextField() = assertCollectionNameTextField()
fun verifyFindInPageButton() = assertFindInPageButton()
fun verifyShareScrim() = assertShareScrim()
fun verifySendToDeviceTitle() = assertSendToDeviceTitle()
@ -137,11 +127,11 @@ class ThreeDotMenuMainRobot {
private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
fun openSettings(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
var maxSwipes = 3
while (!settingsButton().exists() && maxSwipes != 0) {
threeDotMenuRecyclerView().perform(swipeUp())
maxSwipes--
}
// We require one swipe to display the full size 3-dot menu. On smaller devices
// such as the Pixel 2, we require two swipes to display the "Settings" menu item
// at the bottom. On larger devices, the second swipe is a no-op.
threeDotMenuRecyclerView().perform(swipeUp())
threeDotMenuRecyclerView().perform(swipeUp())
settingsButton().click()
SettingsRobot().interact()
@ -156,15 +146,6 @@ class ThreeDotMenuMainRobot {
return DownloadRobot.Transition()
}
fun openSyncedTabs(interact: SyncedTabsRobot.() -> Unit): SyncedTabsRobot.Transition {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("Synced tabs")), waitingTime)
syncedTabsButton().click()
SyncedTabsRobot().interact()
return SyncedTabsRobot.Transition()
}
fun openSyncSignIn(interact: SyncSignInRobot.() -> Unit): SyncSignInRobot.Transition {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("Sign in to sync")), waitingTime)
@ -179,7 +160,7 @@ class ThreeDotMenuMainRobot {
mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime)
bookmarksButton().click()
assertTrue(mDevice.findObject(UiSelector().resourceId("org.mozilla.fenix.debug:id/bookmark_list")).waitForExists(waitingTime))
assertTrue(mDevice.findObject(UiSelector().resourceId("$packageName:id/bookmark_list")).waitForExists(waitingTime))
BookmarksRobot().interact()
return BookmarksRobot.Transition()
@ -296,26 +277,6 @@ class ThreeDotMenuMainRobot {
return BrowserRobot.Transition()
}
fun typeCollectionName(
name: String,
interact: BrowserRobot.() -> Unit
): BrowserRobot.Transition {
mDevice.wait(
Until.findObject(By.res("org.mozilla.fenix.debug:id/name_collection_edittext")),
waitingTime
)
collectionNameTextField().perform(
ViewActions.replaceText(name),
ViewActions.pressImeActionButton()
)
// wait for the collection creation wrapper to be dismissed
mDevice.waitNotNull(Until.gone(By.res("org.mozilla.fenix.debug:id/createCollectionWrapper")))
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun openReaderViewAppearance(interact: ReaderViewRobot.() -> Unit): ReaderViewRobot.Transition {
var maxSwipes = 3
while (!readerViewAppearanceToggle().exists() && maxSwipes != 0) {
@ -355,15 +316,7 @@ class ThreeDotMenuMainRobot {
return AddToHomeScreenRobot.Transition()
}
fun selectExistingCollection(title: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.text(title)), waitingTime)
onView(withText(title)).click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun openSaveToCollection(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition {
fun openSaveToCollection(interact: CollectionRobot.() -> Unit): CollectionRobot.Transition {
// Ensure the menu is expanded and fully scrolled to the bottom.
for (i in 0..3) {
threeDotMenuRecyclerView().perform(swipeUp())
@ -371,8 +324,8 @@ class ThreeDotMenuMainRobot {
mDevice.waitNotNull(Until.findObject(By.text("Save to collection")), waitingTime)
saveCollectionButton().click()
ThreeDotMenuMainRobot().interact()
return ThreeDotMenuMainRobot.Transition()
CollectionRobot().interact()
return CollectionRobot.Transition()
}
fun openAddonsManagerMenu(interact: SettingsSubMenuAddonsManagerRobot.() -> Unit): SettingsSubMenuAddonsManagerRobot.Transition {
@ -384,13 +337,6 @@ class ThreeDotMenuMainRobot {
SettingsSubMenuAddonsManagerRobot().interact()
return SettingsSubMenuAddonsManagerRobot.Transition()
}
fun exitSaveCollection(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
exitSaveCollectionButton().click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
}
}
private fun threeDotMenuRecyclerView() =
@ -417,10 +363,6 @@ private fun bookmarksButton() = onView(allOf(withText(R.string.library_bookmarks
private fun assertBookmarksButton() = bookmarksButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun syncedTabsButton() = onView(allOf(withText(R.string.library_synced_tabs)))
private fun assertSyncedTabsButton() = syncedTabsButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun signInToSyncButton() = onView(withText("Sign in to sync"))
private fun assertSignInToSyncButton() = signInToSyncButton().check(matches(isDisplayed()))
@ -457,13 +399,6 @@ private fun assertShareTabButton() = shareTabButton()
private fun shareButton() = mDevice.findObject(UiSelector().description("Share"))
private fun assertShareButton() = assertTrue(shareButton().waitForExists(waitingTime))
private fun browserViewSaveCollectionButton() = onView(
allOf(
withText("Save to collection"),
withEffectiveVisibility(Visibility.VISIBLE)
)
)
private fun saveCollectionButton() = onView(allOf(withText("Save to collection"))).inRoot(RootMatchers.isPlatformPopup())
private fun assertSaveCollectionButton() = saveCollectionButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
@ -472,16 +407,6 @@ private fun selectTabsButton() = onView(allOf(withText("Select tabs"))).inRoot(R
private fun assertSelectTabsButton() = selectTabsButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun addNewCollectionButton() = onView(allOf(withText("Add new collection")))
private fun assertaddNewCollectionButton() = addNewCollectionButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun collectionNameTextField() =
onView(allOf(withResourceName("name_collection_edittext")))
private fun assertCollectionNameTextField() = collectionNameTextField()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun reportSiteIssueButton() = onView(withText("Report Site Issue…"))
private fun assertReportSiteIssueButton() = reportSiteIssueButton().check(matches(isDisplayed()))
@ -581,15 +506,14 @@ private fun clickAddonsManagerButton() {
addOnsButton().check(matches(isCompletelyDisplayed())).click()
}
private fun exitSaveCollectionButton() = onView(withId(R.id.back_button)).check(matches(isDisplayed()))
private fun tabSettingsButton() =
onView(allOf(withText("Tab settings"))).inRoot(RootMatchers.isPlatformPopup())
private fun assertTabSettingsButton() {
tabSettingsButton()
.check(
matches(isDisplayed()))
matches(isDisplayed())
)
}
private fun recentlyClosedTabsButton() =
@ -598,7 +522,8 @@ private fun recentlyClosedTabsButton() =
private fun assertRecentlyClosedTabsButton() {
recentlyClosedTabsButton()
.check(
matches(isDisplayed()))
matches(isDisplayed())
)
}
private fun shareAllTabsButton() =
@ -607,7 +532,8 @@ private fun shareAllTabsButton() =
private fun assertShareAllTabsButton() {
shareAllTabsButton()
.check(
matches(isDisplayed()))
matches(isDisplayed())
)
}
private fun assertNewTabButton() = onView(withText("New tab")).check(matches(isDisplayed()))

@ -7,3 +7,6 @@ package org.mozilla.fenix.ui.util
const val STRING_ONBOARDING_ACCOUNT_SIGN_IN_HEADER = "Sync Firefox between devices"
const val STRING_ONBOARDING_TRACKING_PROTECTION_HEADER = "Always-on privacy"
const val STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER = "Pick your toolbar placement"
const val FRENCH_LANGUAGE_HEADER = "Langues"
const val ROMANIAN_LANGUAGE_HEADER = "Limbă"
const val FRENCH_SYSTEM_LOCALE_OPTION = "Utiliser la langue de lappareil"

@ -0,0 +1,44 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.mozilla.fenix"
android:sharedUserId="${sharedUserId}">
<application
android:name="org.mozilla.fenix.MigratingFenixApplication"
tools:replace="android:name">
<activity android:name=".autofill.AutofillUnlockActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.Translucent" />
<activity android:name=".autofill.AutofillConfirmActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.Translucent" />
<activity android:name=".autofill.AutofillSearchActivity"
android:exported="false"
android:theme="@style/DialogActivityTheme" />
<service
android:name=".autofill.AutofillService"
android:label="@string/app_name"
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
<intent-filter>
<action android:name="android.service.autofill.AutofillService"/>
</intent-filter>
</service>
<!-- Overriding the alias of the main manifest to route app launches through our
MigrationDecisionActivity which will show the migration screen before launching
into the app if needed. -->
<activity-alias
android:name="${applicationId}.App"
android:targetActivity="org.mozilla.fenix.MigrationDecisionActivity"
tools:replace="android:targetActivity" />
<activity
android:name="org.mozilla.fenix.MigrationDecisionActivity"
android:exported="false" />
<service android:name="org.mozilla.fenix.MigrationService" />
</application>
</manifest>

@ -22,12 +22,16 @@
<activity android:name=".autofill.AutofillUnlockActivity"
android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
android:theme="@style/Theme.AppCompat.Translucent" />
<activity android:name=".autofill.AutofillConfirmActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.Translucent" />
<activity android:name=".autofill.AutofillSearchActivity"
android:exported="false"
android:theme="@style/DialogActivityTheme" />
<service
android:name=".autofill.AutofillService"
android:label="@string/app_name"

@ -14,24 +14,33 @@
}
},
{
"slug": "a1",
"slug": "fancy-settings",
"ratio": 0,
"feature": {
"value": {
"settings-title": "settings_title",
"settings-title-punctuation": "…"
"settings-title": "Fancy Settings"
},
"enabled": true,
"featureId": "nimbus-validation"
}
},
{
"slug": "a2",
"slug": "smiley",
"ratio": 0,
"feature": {
"value": {
"settings-title": "preferences_category_general",
"settings-title-punctuation": "!"
"settings-title-punctuation": "\uD83D\uDE03"
},
"enabled": true,
"featureId": "nimbus-validation"
}
},
{
"slug": "bundled-text",
"ratio": 0,
"feature": {
"value": {
"settings-title": "preferences_category_general"
},
"enabled": true,
"featureId": "nimbus-validation"
@ -78,11 +87,11 @@
}
},
{
"slug": "treatment",
"slug": "edit-menu-icon",
"ratio": 0,
"feature": {
"value": {
"settings-title": "Fancy Settings",
"settings-title": "preferences_category_general",
"settings-icon": "ic_edit"
},
"enabled": true,

@ -1,101 +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/. */
import android.content.Context
import android.os.Bundle
import mozilla.components.browser.engine.gecko.autofill.GeckoLoginDelegateWrapper
import mozilla.components.browser.engine.gecko.ext.toContentBlockingSetting
import mozilla.components.browser.engine.gecko.glean.GeckoAdapter
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
import mozilla.components.concept.storage.LoginsStorage
import mozilla.components.lib.crash.handler.CrashHandlerService
import mozilla.components.service.sync.logins.GeckoLoginStorageDelegate
import org.mozilla.fenix.Config
import org.mozilla.fenix.ext.components
import org.mozilla.geckoview.ContentBlocking
import org.mozilla.geckoview.GeckoRuntime
import org.mozilla.geckoview.GeckoRuntimeSettings
import org.mozilla.geckoview.ContentBlocking.SafeBrowsingProvider
object GeckoProvider {
var testConfig: Bundle? = null
private var runtime: GeckoRuntime? = null
const val CN_UPDATE_URL =
"https://sb.firefox.com.cn/downloads?client=SAFEBROWSING_ID&appver=%MAJOR_VERSION%&pver=2.2"
const val CN_GET_HASH_URL =
"https://sb.firefox.com.cn/gethash?client=SAFEBROWSING_ID&appver=%MAJOR_VERSION%&pver=2.2"
@Synchronized
fun getOrCreateRuntime(
context: Context,
storage: Lazy<LoginsStorage>,
trackingProtectionPolicy: TrackingProtectionPolicy
): GeckoRuntime {
if (runtime == null) {
runtime = createRuntime(context, storage, trackingProtectionPolicy)
}
return runtime!!
}
private fun createRuntime(
context: Context,
storage: Lazy<LoginsStorage>,
policy: TrackingProtectionPolicy
): GeckoRuntime {
val builder = GeckoRuntimeSettings.Builder()
testConfig?.let {
builder.extras(it)
.remoteDebuggingEnabled(true)
}
// Use meeee.
policy.hashCode()
val runtimeSettings = builder
.crashHandler(CrashHandlerService::class.java)
.telemetryDelegate(GeckoAdapter())
.contentBlocking(policy.toContentBlockingSetting())
.aboutConfigEnabled(true)
.debugLogging(Config.channel.isDebug)
.build()
val settings = context.components.settings
if (!settings.shouldUseAutoSize) {
runtimeSettings.automaticFontSizeAdjustment = false
val fontSize = settings.fontSizeFactor
runtimeSettings.fontSizeFactor = fontSize
}
// Add safebrowsing providers for China
if (Config.channel.isMozillaOnline) {
val mozcn = SafeBrowsingProvider
.withName("mozcn")
.version("2.2")
.lists("m6eb-phish-shavar", "m6ib-phish-shavar")
.updateUrl(CN_UPDATE_URL)
.getHashUrl(CN_GET_HASH_URL)
.build()
runtimeSettings.contentBlocking.setSafeBrowsingProviders(mozcn,
// Keep the existing configuration
ContentBlocking.GOOGLE_SAFE_BROWSING_PROVIDER,
ContentBlocking.GOOGLE_LEGACY_SAFE_BROWSING_PROVIDER)
runtimeSettings.contentBlocking.setSafeBrowsingPhishingTable(
"m6eb-phish-shavar",
"m6ib-phish-shavar",
// Existing configuration
"goog-phish-proto")
}
val geckoRuntime = GeckoRuntime.create(context, runtimeSettings)
val loginStorageDelegate = GeckoLoginStorageDelegate(storage)
@Suppress("Deprecation")
geckoRuntime.loginStorageDelegate = GeckoLoginDelegateWrapper(loginStorageDelegate)
return geckoRuntime
}
}

@ -1,61 +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/. */
const ADLINK_CHECK_TIMEOUT_MS = 1000;
function collectLinks(urls) {
let anchors = document.getElementsByTagName("a");
for (let anchor of anchors) {
if (!anchor.href) {
continue;
}
urls.push(anchor.href);
}
}
function sendLinks(cookies) {
let urls = [];
collectLinks(urls);
let message = {
'url': document.location.href,
'urls': urls,
'cookies': cookies
};
browser.runtime.sendNativeMessage("MozacBrowserAds", message);
}
function notify(message) {
sendLinks(message.cookies);
}
browser.runtime.onMessage.addListener(notify);
const events = ["pageshow", "load", "unload"];
var timeout;
const eventLogger = event => {
switch (event.type) {
case "load":
timeout = setTimeout(() => {
browser.runtime.sendMessage({ "checkCookies": true });
}, ADLINK_CHECK_TIMEOUT_MS)
break;
case "pageshow":
if (event.persisted) {
timeout = setTimeout(() => {
browser.runtime.sendMessage({ "checkCookies": true });
}, ADLINK_CHECK_TIMEOUT_MS)
}
break;
case "unload":
clearTimeout(timeout);
default:
console.log('Event:', event.type);
}
};
events.forEach(eventName =>
window.addEventListener(eventName, eventLogger)
);

@ -1,28 +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/. */
browser.runtime.onMessage.addListener(notify);
function sendMessageToTabs(tabs, cookies) {
for (let tab of tabs) {
browser.tabs.sendMessage(
tab.id,
{ cookies }
);
}
}
function notify(message) {
if (message.checkCookies) {
browser.cookies.getAll({})
.then(cookies => {
browser.tabs.query({
currentWindow: true,
active: true
}).then(tabs => {
sendMessageToTabs(tabs, cookies);
});
});
}
}

@ -1,38 +0,0 @@
{
"manifest_version": 2,
"applications": {
"gecko": {
"id": "ads@mozac.org"
}
},
"name": "Mozilla Android Components - Ads",
"version": "${version}",
"content_scripts": [
{
"matches": ["https://*/*"],
"include_globs": [
"https://www.google.*/search*",
"https://www.bing.com/search*",
"https://duckduckgo.com/*"
],
"js": ["ads.js"],
"run_at": "document_end"
}
],
"background": {
"scripts": ["adsBackground.js"]
},
"permissions": [
"geckoViewAddons",
"nativeMessaging",
"nativeMessagingFromContent",
"geckoViewAddons",
"nativeMessaging",
"nativeMessagingFromContent",
"webNavigation",
"webRequest",
"webRequestBlocking",
"cookies",
"*://*/*"
]
}

@ -1,47 +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/. */
const COOKIES_CHECK_TIMEOUT_MS = 1000;
function sendCookies(cookies) {
let message = {
'url': document.location.href,
'cookies': cookies
}
browser.runtime.sendNativeMessage("BrowserCookiesMessage", message);
}
function notify(message) {
sendCookies(message.cookies);
}
browser.runtime.onMessage.addListener(notify);
const events = ["pageshow", "load", "unload"];
var timeout;
const eventLogger = event => {
switch (event.type) {
case "load":
timeout = setTimeout(() => {
browser.runtime.sendMessage({"checkCookies": true});
}, COOKIES_CHECK_TIMEOUT_MS);
break;
case "pageshow":
if (event.persisted) {
timeout = setTimeout(() => {
browser.runtime.sendMessage({"checkCookies": true});
}, COOKIES_CHECK_TIMEOUT_MS);
}
break;
case "unload":
clearTimeout(timeout);
default:
console.log('Event:', event.type);
}
};
events.forEach(eventName =>
window.addEventListener(eventName, eventLogger)
);

@ -1,28 +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/. */
browser.runtime.onMessage.addListener(notify);
function sendMessageToTabs(tabs, cookies) {
for (let tab of tabs) {
browser.tabs.sendMessage(
tab.id,
{cookies: cookies}
);
}
}
function notify(message) {
if(message.checkCookies) {
browser.cookies.getAll({})
.then(cookies => {
browser.tabs.query({
currentWindow: true,
active: true
}).then(tabs => {
sendMessageToTabs(tabs, cookies);
});
});
}
}

@ -1,38 +0,0 @@
{
"manifest_version": 2,
"applications": {
"gecko": {
"id": "cookies@mozac.org"
}
},
"name": "Mozilla Android Components - Cookies",
"version": "${version}",
"content_scripts": [
{
"matches": ["https://*/*"],
"include_globs": [
"https://www.google.*/search*",
"https://www.baidu.com/from=844b/s*",
"https://www.baidu.com/from=844b/baidu*",
"https://*search.yahoo.com/search*",
"https://www.bing.com/search*",
"https://duckduckgo.com/*"
],
"js": ["cookies.js"],
"run_at": "document_end"
}
],
"background": {
"scripts": ["cookiesBackground.js"]
},
"permissions": [
"geckoViewAddons",
"nativeMessaging",
"nativeMessagingFromContent",
"webNavigation",
"webRequest",
"webRequestBlocking",
"cookies",
"*://*/*"
]
}

@ -37,4 +37,7 @@ public class Adjust {
public static void setEnabled(boolean enabled) {
}
public static void gdprForgetMe(Object ignored) {
}
}

@ -15,7 +15,6 @@ import mozilla.components.concept.engine.request.RequestInterceptor
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.isOnline
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import java.lang.ref.WeakReference
class AppRequestInterceptor(
@ -98,7 +97,7 @@ class AppRequestInterceptor(
// Navigate and trigger add-on installation.
matchResult.groupValues.getOrNull(1)?.let { addonId ->
navController?.get()?.navigateBlockingForAsyncNavGraph(
navController?.get()?.navigate(
NavGraphDirections.actionGlobalAddonsManagementFragment(addonId)
)

@ -18,7 +18,6 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) {
FromHome(R.id.homeFragment),
FromSearchDialog(R.id.searchDialogFragment),
FromSettings(R.id.settingsFragment),
FromSyncedTabs(R.id.syncedTabsFragment),
FromBookmarks(R.id.bookmarkFragment),
FromHistory(R.id.historyFragment),
FromTrackingProtectionExceptions(R.id.trackingProtectionExceptionsFragment),
@ -31,7 +30,6 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) {
FromAddonDetailsFragment(R.id.addonDetailsFragment),
FromAddonPermissionsDetailsFragment(R.id.addonPermissionsDetailFragment),
FromLoginDetailFragment(R.id.loginDetailFragment),
FromTabTrayDialog(R.id.tabTrayDialogFragment),
FromTabTray(R.id.tabsTrayFragment),
FromTabsTray(R.id.tabsTrayFragment),
FromRecentlyClosed(R.id.recentlyClosedFragment)
}

@ -14,11 +14,6 @@ object FeatureFlags {
*/
const val pullToRefreshEnabled = true
/**
* Enables the Nimbus experiments library.
*/
const val nimbusExperiments = false
/**
* Enables the Addresses autofill feature.
*/
@ -35,12 +30,45 @@ object FeatureFlags {
const val webAuthFeature = true
/**
* Shows new three-dot toolbar menu design.
* Enables the Home button in the browser toolbar to navigate back to the home screen.
*/
val showHomeButtonFeature = Config.channel.isNightlyOrDebug
/**
* Enables the Start On Home feature in the settings page.
*/
val showStartOnHomeSettings = Config.channel.isNightlyOrDebug
/**
* Enables the "recent" tabs feature in the home screen.
*/
val showRecentTabsFeature = Config.channel.isNightlyOrDebug
/**
* Enables recording of history metadata.
*/
val historyMetadataFeature = Config.channel.isDebug
/**
* Enables the recently saved bookmarks feature in the home screen.
*/
val recentBookmarksFeature = Config.channel.isNightlyOrDebug
/**
* Identifies and separates the tabs list with a secondary section containing least used tabs.
*/
val inactiveTabs = Config.channel.isNightlyOrDebug
/**
* Enables support for Android Autofill.
*
* In addition to toggling this flag, matching entries in the Android Manifest of the build
* type need to present.
*/
const val toolbarMenuFeature = true
val androidAutofill = Config.channel.isNightlyOrDebug || Config.channel.isBeta
/**
* Enables the tabs tray re-write with Synced Tabs.
* Enables showing the home screen behind the search dialog
*/
const val tabsTrayRewrite = true
val showHomeBehindSearch = Config.channel.isNightlyOrDebug
}

@ -17,6 +17,7 @@ import androidx.lifecycle.ProcessLifecycleOwner
import androidx.work.Configuration.Builder
import androidx.work.Configuration.Provider
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
@ -64,10 +65,12 @@ import org.mozilla.fenix.telemetry.TelemetryLifecycleObserver
import org.mozilla.fenix.utils.BrowsersCache
import java.util.concurrent.TimeUnit
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.autofill.AutofillUseCases
import mozilla.components.feature.search.ext.buildSearchUrl
import mozilla.components.feature.search.ext.waitForSelectedOrDefaultSearchEngine
import mozilla.components.service.fxa.manager.SyncEnginesStorage
import org.mozilla.fenix.GleanMetrics.Addons
import org.mozilla.fenix.GleanMetrics.AndroidAutofill
import org.mozilla.fenix.GleanMetrics.Preferences
import org.mozilla.fenix.GleanMetrics.SearchDefaultEngine
import org.mozilla.fenix.components.metrics.Event
@ -125,6 +128,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
PerfStartup.applicationOnCreate.stopAndAccumulate(completeMethodDurationTimerId)
}
@OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
protected open fun initializeGlean() {
val telemetryEnabled = settings().isTelemetryEnabled
@ -136,7 +140,8 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
channel = BuildConfig.BUILD_TYPE,
httpClient = ConceptFetchHttpUploader(
lazy(LazyThreadSafetyMode.NONE) { components.core.client }
)),
)
),
uploadEnabled = telemetryEnabled,
buildInfo = GleanBuildInfo.buildInfo
)
@ -205,11 +210,10 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
initVisualCompletenessQueueAndQueueTasks()
ProcessLifecycleOwner.get().lifecycle.addObserver(TelemetryLifecycleObserver(components.core.store))
components.appStartupTelemetry.onFenixApplicationOnCreate()
}
}
@OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
private fun restoreBrowserState() = GlobalScope.launch(Dispatchers.Main) {
val store = components.core.store
val sessionStorage = components.core.sessionStorage
@ -235,6 +239,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
registerActivityLifecycleCallbacks(PerformanceActivityLifecycleCallbacks(queue))
}
@OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
fun queueInitStorageAndServices() {
components.performance.visualCompletenessQueue.queue.runIfReadyOrQueue {
GlobalScope.launch(Dispatchers.IO) {
@ -268,12 +273,14 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
}
}
@OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
fun queueReviewPrompt() {
GlobalScope.launch(Dispatchers.IO) {
components.reviewPromptController.trackApplicationLaunch()
}
}
@OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
fun queueRestoreLocale() {
components.performance.visualCompletenessQueue.queue.runIfReadyOrQueue {
GlobalScope.launch(Dispatchers.IO) {
@ -306,6 +313,8 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
// To re-enable this, we need to do so in a way that won't interfere with any startup operations
// which acquire reserved+ sqlite lock. Currently, Fennec migrations need to write to storage
// on startup, and since they run in a background service we can't simply order these operations.
@OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
private fun runStorageMaintenance() {
GlobalScope.launch(Dispatchers.IO) {
// Bookmarks and history storage sit on top of the same db file so we only need to
@ -360,6 +369,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
* - https://github.com/mozilla/application-services/blob/master/docs/design/megazords.md
* - https://mozilla.github.io/application-services/docs/applications/consuming-megazord-libraries.html
*/
@OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
private fun setupMegazord(): Deferred<Unit> {
// Note: Megazord.init() must be called as soon as possible ...
Megazord.init()
@ -383,15 +393,17 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
logger.info("onTrimMemory(), level=$level, main=${isMainProcess()}")
components.analytics.crashReporter.recordCrashBreadcrumb(Breadcrumb(
category = "Memory",
message = "onTrimMemory()",
data = mapOf(
"level" to level.toString(),
"main" to isMainProcess().toString()
),
level = Breadcrumb.Level.INFO
))
components.analytics.crashReporter.recordCrashBreadcrumb(
Breadcrumb(
category = "Memory",
message = "onTrimMemory()",
data = mapOf(
"level" to level.toString(),
"main" to isMainProcess().toString()
),
level = Breadcrumb.Level.INFO
)
)
runOnlyInMainProcess {
components.core.icons.onTrimMemory(level)
@ -456,6 +468,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
}
}
@OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
private fun warmBrowsersCache() {
// We avoid blocking the main thread for BrowsersCache on startup by loading it on
// background thread.
@ -478,29 +491,24 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
components.core.store,
onNewTabOverride = {
_, engineSession, url ->
val shouldCreatePrivateSession =
components.core.store.state.selectedTab?.content?.private
?: components.settings.openLinksInAPrivateTab
if (shouldCreatePrivateSession) {
components.useCases.tabsUseCases.addPrivateTab(
url = url,
selectTab = true,
engineSession = engineSession
)
} else {
components.useCases.tabsUseCases.addTab(
url = url,
selectTab = true,
engineSession = engineSession
)
}
val shouldCreatePrivateSession =
components.core.store.state.selectedTab?.content?.private
?: components.settings.openLinksInAPrivateTab
components.useCases.tabsUseCases.addTab(
url = url,
selectTab = true,
engineSession = engineSession,
private = shouldCreatePrivateSession
)
},
onCloseTabOverride = {
_, sessionId -> components.useCases.tabsUseCases.removeTab(sessionId)
_, sessionId ->
components.useCases.tabsUseCases.removeTab(sessionId)
},
onSelectTabOverride = {
_, sessionId -> components.useCases.tabsUseCases.selectTab(sessionId)
_, sessionId ->
components.useCases.tabsUseCases.selectTab(sessionId)
},
onExtensionsLoaded = { extensions ->
components.addonUpdater.registerForFutureUpdates(extensions)
@ -580,18 +588,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
topSitesCount.add(topSitesSize)
}
if (settings.creditCardsSavedCount > 0) {
creditCardsSavedCount.add(settings.creditCardsSavedCount)
}
if (settings.creditCardsDeletedCount > 0) {
creditCardsDeletedCount.add(settings.creditCardsDeletedCount)
}
if (settings.creditCardsAutofilledCount > 0) {
creditCardsAutofillCount.add(settings.creditCardsAutofilledCount)
}
val installedAddonSize = settings.installedAddonsCount
Addons.hasInstalledAddons.set(installedAddonSize > 0)
if (installedAddonSize > 0) {
@ -627,6 +623,12 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
closeTabSetting.set(settings.getTabTimeoutPingString())
}
with(AndroidAutofill) {
val autofillUseCases = AutofillUseCases()
supported.set(autofillUseCases.isSupported(applicationContext))
enabled.set(autofillUseCases.isEnabled(applicationContext))
}
browserStore.waitForSelectedOrDefaultSearchEngine { searchEngine ->
if (searchEngine != null) {
SearchDefaultEngine.apply {
@ -650,8 +652,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
bookmarksSuggestion.set(settings.shouldShowBookmarkSuggestions)
clipboardSuggestionsEnabled.set(settings.shouldShowClipboardSuggestions)
searchShortcutsEnabled.set(settings.shouldShowSearchShortcuts)
openLinksInPrivate.set(settings.openLinksInAPrivateTab)
privateSearchSuggestions.set(settings.shouldShowSearchSuggestionsInPrivate)
voiceSearchEnabled.set(settings.shouldShowVoiceSearch)
openLinksInAppEnabled.set(settings.openLinksInExternalApp)
signedInSync.set(settings.signedInFxaAccount)

@ -6,6 +6,7 @@ package org.mozilla.fenix
import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_MAIN
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
@ -78,7 +79,6 @@ import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExcepti
import org.mozilla.fenix.ext.alreadyOnDestination
import org.mozilla.fenix.ext.breadcrumb
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.measureNoInline
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav
@ -86,6 +86,7 @@ import org.mozilla.fenix.ext.setNavigationIcon
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.home.intent.CrashReporterIntentProcessor
import org.mozilla.fenix.home.intent.DefaultBrowserIntentProcessor
import org.mozilla.fenix.home.intent.OpenBrowserIntentProcessor
import org.mozilla.fenix.home.intent.OpenSpecificTabIntentProcessor
import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor
@ -94,7 +95,7 @@ import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections
import org.mozilla.fenix.library.bookmarks.DesktopFolders
import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections
import org.mozilla.fenix.perf.NavGraphProvider
import org.mozilla.fenix.onboarding.DefaultBrowserNotificationWorker
import org.mozilla.fenix.perf.Performance
import org.mozilla.fenix.perf.PerformanceInflater
import org.mozilla.fenix.perf.ProfilerMarkers
@ -111,13 +112,12 @@ import org.mozilla.fenix.settings.logins.fragment.SavedLoginsAuthFragmentDirecti
import org.mozilla.fenix.settings.search.AddSearchEngineFragmentDirections
import org.mozilla.fenix.settings.search.EditCustomSearchEngineFragmentDirections
import org.mozilla.fenix.share.AddNewDeviceFragmentDirections
import org.mozilla.fenix.sync.SyncedTabsFragmentDirections
import org.mozilla.fenix.tabstray.TabsTrayFragment
import org.mozilla.fenix.tabstray.TabsTrayFragmentDirections
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentDirections
import org.mozilla.fenix.theme.DefaultThemeManager
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.BrowsersCache
import org.mozilla.fenix.utils.Settings
import java.lang.ref.WeakReference
/**
@ -160,7 +160,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
SpeechProcessingIntentProcessor(this, components.core.store, components.analytics.metrics),
StartSearchIntentProcessor(components.analytics.metrics),
OpenBrowserIntentProcessor(this, ::getIntentSessionId),
OpenSpecificTabIntentProcessor(this)
OpenSpecificTabIntentProcessor(this),
DefaultBrowserIntentProcessor(this, components.analytics.metrics)
)
}
@ -199,11 +200,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.publicSuffixList.prefetch()
setContentView(R.layout.activity_home).run {
// Do not call anything between setContentView and inflateNavGraphAsync.
// It needs to start its job as early as possible.
NavGraphProvider.inflateNavGraphAsync(navHost.navController, lifecycleScope)
}
setContentView(R.layout.activity_home)
// Must be after we set the content view
if (isVisuallyComplete) {
@ -219,9 +216,12 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
it.start()
}
if (isActivityColdStarted(intent, savedInstanceState) &&
!externalSourceIntentProcessors.any { it.process(intent, navHost.navController, this.intent) }) {
if (!shouldStartOnHome() &&
shouldNavigateBrowserFragmentOnCouldStart(savedInstanceState)
) {
navigateToBrowserOnColdStart()
} else if (FeatureFlags.showStartOnHomeSettings) {
components.analytics.metrics.track(Event.StartOnHomeEnterHomeScreen)
}
Performance.processIntentIfPerformanceTest(intent, this)
@ -238,10 +238,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
safeIntent
?.let(::getIntentSource)
?.also { components.analytics.metrics.track(Event.OpenedApp(it)) }
// record on cold startup
safeIntent
?.let(::getIntentAllSource)
?.also { components.analytics.metrics.track(Event.AppReceivedIntent(it)) }
}
supportActionBar?.hide()
@ -257,7 +253,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
captureSnapshotTelemetryMetrics()
startupTelemetryOnCreateCalled(intent.toSafeIntent(), savedInstanceState != null)
startupTelemetryOnCreateCalled(intent.toSafeIntent())
startupPathProvider.attachOnActivityOnCreate(lifecycle, intent)
startupTypeTelemetry = StartupTypeTelemetry(components.startupStateProvider, startupPathProvider).apply {
attachOnHomeActivityOnCreate(lifecycle)
@ -268,17 +264,9 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
StartupTimeline.onActivityCreateEndHome(this) // DO NOT MOVE ANYTHING BELOW HERE.
}
protected open fun startupTelemetryOnCreateCalled(
safeIntent: SafeIntent,
hasSavedInstanceState: Boolean
) {
// This function gets overridden by subclasses.
components.appStartupTelemetry.onHomeActivityOnCreate(
safeIntent,
hasSavedInstanceState,
homeActivityInitTimeStampNanoSeconds, rootContainer
)
private fun startupTelemetryOnCreateCalled(safeIntent: SafeIntent) {
// We intentionally only record this in HomeActivity and not ExternalBrowserActivity (e.g.
// PWAs) so we don't include more unpredictable code paths in the results.
components.performance.coldStartupDurationTelemetry.onHomeActivityOnCreate(
components.performance.visualCompletenessQueue,
components.startupStateProvider,
@ -287,17 +275,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
)
}
override fun onRestart() {
// DO NOT MOVE ANYTHING ABOVE THIS..
// we are measuring startup time for hot startup type
startupTelemetryOnRestartCalled()
super.onRestart()
}
private fun startupTelemetryOnRestartCalled() {
components.appStartupTelemetry.onHomeActivityOnRestart(rootContainer)
}
@CallSuper
override fun onResume() {
super.onResume()
@ -330,17 +307,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
}
}
// Launch this on a background thread so as not to affect startup performance
lifecycleScope.launch(IO) {
if (
settings().isDefaultBrowser() &&
settings().wasDefaultBrowserOnLastResume != settings().isDefaultBrowser()
) {
metrics.track(Event.ChangedToDefaultBrowser)
}
settings().wasDefaultBrowserOnLastResume = settings().isDefaultBrowser()
}
isFenixTheDefaultBrowser()
}
override fun onStart() = PerfStartup.homeActivityOnStart.measureNoInline {
@ -366,8 +333,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
"finishing" to isFinishing.toString()
)
)
components.appStartupTelemetry.onStop()
}
final override fun onPause() {
@ -502,19 +467,9 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
?.childFragmentManager
?.fragments
?.lastOrNull()
?.let { it as? TabTrayDialogFragment }
?.let { it as? TabsTrayFragment }
?.also { it.dismissAllowingStateLoss() }
}
// Note: This does not work in case of an user sending an intent with ACTION_VIEW
// for example, launch the application, and than use adb to send an intent with
// ACTION_VIEW to open a link. In this case, we will get multiple telemetry events.
intent
.toSafeIntent()
.let(::getIntentAllSource)
?.also { components.analytics.metrics.track(Event.AppReceivedIntent(it)) }
components.appStartupTelemetry.onHomeActivityOnNewIntent(intent.toSafeIntent())
}
/**
@ -585,6 +540,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
super.onBackPressed()
}
@Suppress("DEPRECATION")
// https://github.com/mozilla-mobile/fenix/issues/19919
final override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach {
if (it is ActivityResultHandler && it.onActivityResult(requestCode, data, resultCode)) {
@ -668,14 +625,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
}
}
protected open fun getIntentAllSource(intent: SafeIntent): Event.AppReceivedIntent.Source? {
return when {
intent.isLauncherIntent -> Event.AppReceivedIntent.Source.APP_ICON
intent.action == Intent.ACTION_VIEW -> Event.AppReceivedIntent.Source.LINK
else -> Event.AppReceivedIntent.Source.UNKNOWN
}
}
/**
* External sources such as 3rd party links and shortcuts use this function to enter
* private mode directly before the content view is created. Returns the mode set by the intent
@ -787,8 +736,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
SearchDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromSettings ->
SettingsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromSyncedTabs ->
SyncedTabsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromBookmarks ->
BookmarkFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromHistory ->
@ -813,9 +760,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
AddonPermissionsDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromLoginDetailFragment ->
LoginDetailFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTabTrayDialog ->
TabTrayDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTabTray ->
BrowserDirection.FromTabsTray ->
TabsTrayFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromRecentlyClosed ->
RecentlyClosedFragmentDirections.actionGlobalBrowser(customTabSessionId)
@ -837,28 +782,38 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
val startTime = components.core.engine.profiler?.getProfilerTime()
val mode = browsingModeManager.mode
val loadUrlUseCase = if (newTab) {
when (mode) {
BrowsingMode.Private -> components.useCases.tabsUseCases.addPrivateTab
BrowsingMode.Normal -> components.useCases.tabsUseCases.addTab
}
} else components.useCases.sessionUseCases.loadUrl
val private = when (mode) {
BrowsingMode.Private -> true
BrowsingMode.Normal -> false
}
// In situations where we want to perform a search but have no search engine (e.g. the user
// has removed all of them, or we couldn't load any) we will pass searchTermOrURL to Gecko
// and let it try to load whatever was entered.
if ((!forceSearch && searchTermOrURL.isUrl()) || engine == null) {
loadUrlUseCase.invoke(searchTermOrURL.toNormalizedUrl(), flags)
val tabId = if (newTab) {
components.useCases.tabsUseCases.addTab(
url = searchTermOrURL.toNormalizedUrl(),
flags = flags,
private = private
)
} else {
components.useCases.sessionUseCases.loadUrl(
url = searchTermOrURL.toNormalizedUrl(),
flags = flags
)
components.core.store.state.selectedTabId
}
if (requestDesktopMode) {
handleRequestDesktopMode()
if (requestDesktopMode && tabId != null) {
handleRequestDesktopMode(tabId)
}
} else {
if (newTab) {
components.useCases.searchUseCases.newTabSearch
.invoke(
searchTermOrURL,
SessionState.Source.USER_ENTERED,
SessionState.Source.Internal.UserEntered,
true,
mode.isPrivate,
searchEngine = engine
@ -879,15 +834,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
}
}
internal fun handleRequestDesktopMode() {
val requestDesktopSiteUseCase =
components.useCases.sessionUseCases.requestDesktopSite
requestDesktopSiteUseCase.invoke(true)
components.core.store.dispatch(
ContentAction.UpdateDesktopModeAction(
components.core.store.state.selectedTabId.toString(), true
)
)
internal fun handleRequestDesktopMode(tabId: String) {
components.useCases.sessionUseCases.requestDesktopSite(true, tabId)
components.core.store.dispatch(ContentAction.UpdateDesktopModeAction(tabId, true))
// Reset preference value after opening the tab in desktop mode
settings().openNextTabInDesktopMode = false
}
@ -948,7 +898,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
webExtensionId = webExtensionState.id,
webExtensionTitle = webExtensionState.name
)
navHost.navController.navigateBlockingForAsyncNavGraph(action)
navHost.navController.navigate(action)
}
/**
@ -976,14 +926,56 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
}
}
private fun isFenixTheDefaultBrowser() {
// Launch this on a background thread so as not to affect startup performance
lifecycleScope.launch(IO) {
if (
settings().checkIfFenixIsDefaultBrowserOnAppResume()
) {
metrics.track(Event.ChangedToDefaultBrowser)
}
DefaultBrowserNotificationWorker.setDefaultBrowserNotificationIfNeeded(applicationContext)
}
}
@VisibleForTesting
internal fun isActivityColdStarted(startingIntent: Intent, activityIcicle: Bundle?): Boolean {
// First time opening this activity in the task.
// Cold start / start from Recents after back press.
return activityIcicle == null &&
// Activity was restarted from Recents after it was destroyed by Android while in background
// in cases of memory pressure / "Don't keep activities".
startingIntent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY == 0
// Activity was restarted from Recents after it was destroyed by Android while in background
// in cases of memory pressure / "Don't keep activities".
startingIntent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY == 0
}
/**
* Indicates if the user should be redirected to the [BrowserFragment] or to the [HomeFragment],
* links from an external apps should always opened in the [BrowserFragment].
*/
fun shouldStartOnHome(intent: Intent? = this.intent): Boolean {
if (!FeatureFlags.showStartOnHomeSettings) {
return false
}
return components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
// We only want to open on home when users tap the app,
// we want to ignore other cases when the app gets open by users clicking on links.
getSettings().shouldStartOnHome() && intent?.action == ACTION_MAIN
}
}
@VisibleForTesting
internal fun getSettings(): Settings = settings()
private fun shouldNavigateBrowserFragmentOnCouldStart(savedInstanceState: Bundle?): Boolean {
return isActivityColdStarted(intent, savedInstanceState) &&
!externalSourceIntentProcessors.any {
it.process(
intent,
navHost.navController,
this.intent
)
}
}
companion object {

@ -6,10 +6,14 @@ package org.mozilla.fenix
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.StrictMode
import androidx.annotation.VisibleForTesting
import mozilla.components.feature.intent.processing.IntentProcessor
import mozilla.components.support.utils.EXTRA_ACTIVITY_REFERRER_CATEGORY
import mozilla.components.support.utils.EXTRA_ACTIVITY_REFERRER_PACKAGE
import org.mozilla.fenix.HomeActivity.Companion.PRIVATE_BROWSING_MODE
import org.mozilla.fenix.components.IntentProcessorType
import org.mozilla.fenix.components.getType
@ -54,6 +58,8 @@ class IntentReceiverActivity : Activity() {
components.analytics.metrics.track(Event.OpenedLink(Event.OpenedLink.Mode.NORMAL))
}
addReferrerInformation(intent)
val processor = getIntentProcessors(private).firstOrNull { it.process(intent) }
val intentProcessorType = components.intentProcessors.getType(processor)
@ -96,6 +102,28 @@ class IntentReceiverActivity : Activity() {
modeDependentProcessors +
NewTabShortcutIntentProcessor()
}
private fun addReferrerInformation(intent: Intent) {
// Pass along referrer information when possible.
// Referrer is supported for API>=22.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
return
}
// NB: referrer can be spoofed by the calling application. Use with caution.
val r = referrer ?: return
intent.putExtra(EXTRA_ACTIVITY_REFERRER_PACKAGE, r.host)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Category is supported for API>=26.
r.host?.let { host ->
try {
val category = packageManager.getApplicationInfo(host, 0).category
intent.putExtra(EXTRA_ACTIVITY_REFERRER_CATEGORY, category)
} catch (e: PackageManager.NameNotFoundException) {
// At least we tried.
}
}
}
}
}
private fun Intent.stripUnwantedFlags() {

@ -18,7 +18,8 @@ import org.mozilla.fenix.ext.showToolbar
/**
* A fragment to show the permissions of an add-on.
*/
class AddonPermissionsDetailsFragment : Fragment(R.layout.fragment_add_on_permissions),
class AddonPermissionsDetailsFragment :
Fragment(R.layout.fragment_add_on_permissions),
AddonPermissionsDetailsInteractor {
private val args by navArgs<AddonPermissionsDetailsFragmentArgs>()

@ -10,10 +10,10 @@ import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.CustomTabListAction
import mozilla.components.browser.state.state.createCustomTab
import mozilla.components.browser.state.state.CustomTabSessionState
import mozilla.components.browser.state.state.EngineState
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.createCustomTab
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.prompt.PromptRequest
import mozilla.components.concept.engine.window.WindowRequest
@ -33,6 +33,8 @@ abstract class AddonPopupBaseFragment : Fragment(), EngineSession.Observer, User
protected var engineSession: EngineSession? = null
private var canGoBack: Boolean = false
@Suppress("DEPRECATION")
// https://github.com/mozilla-mobile/fenix/issues/19920
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
session?.let {
promptsFeature.set(
@ -43,7 +45,8 @@ abstract class AddonPopupBaseFragment : Fragment(), EngineSession.Observer, User
fragmentManager = parentFragmentManager,
onNeedToRequestPermissions = { permissions ->
requestPermissions(permissions, REQUEST_CODE_PROMPT_PERMISSIONS)
}),
}
),
owner = this,
view = view
)
@ -102,7 +105,10 @@ abstract class AddonPopupBaseFragment : Fragment(), EngineSession.Observer, User
protected fun initializeSession(fromEngineSession: EngineSession? = null) {
engineSession = fromEngineSession ?: requireComponents.core.engine.createSession()
session = createCustomTab("").copy(engineState = EngineState(engineSession))
session = createCustomTab(
url = "",
source = SessionState.Source.Internal.CustomTab
).copy(engineState = EngineState(engineSession))
requireComponents.core.store.dispatch(CustomTabListAction.AddCustomTabAction(session as CustomTabSessionState))
}

@ -119,7 +119,7 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
addons?.forEach { addon ->
val names = addon.translatableName
names["en-US"]?.let { name ->
if (name.toLowerCase(Locale.ENGLISH).contains(addonNameSubStr.toLowerCase(Locale.ENGLISH))) {
if (name.lowercase().contains(addonNameSubStr.lowercase())) {
searchedAddons.add(addon)
}
}

@ -8,7 +8,6 @@ import androidx.navigation.NavController
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.ui.AddonsManagerAdapterDelegate
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.navigateSafe
/**
@ -56,6 +55,6 @@ class AddonsManagementView(
AddonsManagementFragmentDirections.actionAddonsManagementFragmentToNotYetSupportedAddonFragment(
unsupportedAddons.toTypedArray()
)
navController.navigateBlockingForAsyncNavGraph(directions)
navController.navigate(directions)
}
}

@ -25,7 +25,6 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.ext.runIfFragmentIsAttached
@ -205,18 +204,14 @@ class InstalledAddonDetailsFragment : Fragment() {
val shouldCreatePrivateSession =
(activity as HomeActivity).browsingModeManager.mode.isPrivate
if (shouldCreatePrivateSession) {
components.useCases.tabsUseCases.addPrivateTab(settingUrl)
} else {
components.useCases.tabsUseCases.addTab(settingUrl)
}
components.useCases.tabsUseCases.addTab(settingUrl, private = shouldCreatePrivateSession)
InstalledAddonDetailsFragmentDirections.actionGlobalBrowser(null)
} else {
InstalledAddonDetailsFragmentDirections
.actionInstalledAddonFragmentToAddonInternalSettingsFragment(addon)
}
Navigation.findNavController(this).navigateBlockingForAsyncNavGraph(directions)
Navigation.findNavController(this).navigate(directions)
}
}
}
@ -227,7 +222,7 @@ class InstalledAddonDetailsFragment : Fragment() {
InstalledAddonDetailsFragmentDirections.actionInstalledAddonFragmentToAddonDetailsFragment(
addon
)
Navigation.findNavController(view).navigateBlockingForAsyncNavGraph(directions)
Navigation.findNavController(view).navigate(directions)
}
}
@ -237,7 +232,7 @@ class InstalledAddonDetailsFragment : Fragment() {
InstalledAddonDetailsFragmentDirections.actionInstalledAddonFragmentToAddonPermissionsDetailsFragment(
addon
)
Navigation.findNavController(view).navigateBlockingForAsyncNavGraph(directions)
Navigation.findNavController(view).navigate(directions)
}
}

@ -0,0 +1,33 @@
/* 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.autofill
import android.os.Build
import android.os.Bundle
import android.view.ViewGroup
import androidx.annotation.RequiresApi
import mozilla.components.feature.autofill.AutofillConfiguration
import mozilla.components.feature.autofill.ui.AbstractAutofillSearchActivity
import org.mozilla.fenix.ext.components
/**
* Activity responsible for letting the user manually search and pick credentials for auto-filling a
* third-party app.
*/
@RequiresApi(Build.VERSION_CODES.O)
class AutofillSearchActivity : AbstractAutofillSearchActivity() {
override val configuration: AutofillConfiguration by lazy { components.autofillConfiguration }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// To avoid the dialog constantly resizing horizontally while typing, let's always use
// the full width of the screen for the dialog.
window.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
}

@ -77,7 +77,7 @@ import mozilla.components.feature.session.FullScreenFeature
import mozilla.components.feature.session.PictureInPictureFeature
import mozilla.components.feature.session.SessionFeature
import mozilla.components.feature.session.SwipeRefreshFeature
import mozilla.components.feature.sitepermissions.SitePermissions
import mozilla.components.concept.engine.permission.SitePermissions
import mozilla.components.feature.sitepermissions.SitePermissionsFeature
import mozilla.components.lib.state.ext.consumeFlow
import mozilla.components.lib.state.ext.flowScoped
@ -104,9 +104,7 @@ import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.toolbar.BrowserFragmentState
import org.mozilla.fenix.components.toolbar.BrowserFragmentStore
import org.mozilla.fenix.components.toolbar.BrowserInteractor
import org.mozilla.fenix.components.toolbar.BrowserToolbarView
import org.mozilla.fenix.components.toolbar.BrowserToolbarViewInteractor
import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarController
import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarMenuController
import org.mozilla.fenix.components.toolbar.ToolbarIntegration
@ -133,10 +131,11 @@ import java.lang.ref.WeakReference
import mozilla.components.feature.session.behavior.EngineViewBrowserToolbarBehavior
import mozilla.components.feature.webauthn.WebAuthnFeature
import mozilla.components.support.base.feature.ActivityResultHandler
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import mozilla.components.support.ktx.android.view.enterToImmersiveMode
import mozilla.components.support.ktx.kotlin.getOrigin
import org.mozilla.fenix.GleanMetrics.PerfStartup
import org.mozilla.fenix.components.toolbar.interactor.BrowserToolbarInteractor
import org.mozilla.fenix.components.toolbar.interactor.DefaultBrowserToolbarInteractor
import org.mozilla.fenix.ext.measureNoInline
import org.mozilla.fenix.ext.secure
import org.mozilla.fenix.settings.biometric.BiometricPromptFeature
@ -149,15 +148,19 @@ import mozilla.components.feature.session.behavior.ToolbarPosition as MozacToolb
*/
@ExperimentalCoroutinesApi
@Suppress("TooManyFunctions", "LargeClass")
abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, ActivityResultHandler,
OnBackLongPressedListener, AccessibilityManager.AccessibilityStateChangeListener {
abstract class BaseBrowserFragment :
Fragment(),
UserInteractionHandler,
ActivityResultHandler,
OnBackLongPressedListener,
AccessibilityManager.AccessibilityStateChangeListener {
private lateinit var browserFragmentStore: BrowserFragmentStore
private lateinit var browserAnimator: BrowserAnimator
private var _browserInteractor: BrowserToolbarViewInteractor? = null
protected val browserInteractor: BrowserToolbarViewInteractor
get() = _browserInteractor!!
private var _browserToolbarInteractor: BrowserToolbarInteractor? = null
protected val browserToolbarInteractor: BrowserToolbarInteractor
get() = _browserToolbarInteractor!!
@VisibleForTesting
@Suppress("VariableNaming")
@ -236,25 +239,25 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
}
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) =
PerfStartup.baseBfragmentOnViewCreated.measureNoInline { // weird indentation to avoid breaking blame.
initializeUI(view)
if (customTabSessionId == null) {
// We currently only need this observer to navigate to home
// in case all tabs have been removed on startup. No need to
// this if we have a known session to display.
observeRestoreComplete(requireComponents.core.store, findNavController())
}
PerfStartup.baseBfragmentOnViewCreated.measureNoInline { // weird indentation to avoid breaking blame.
initializeUI(view)
if (customTabSessionId == null) {
// We currently only need this observer to navigate to home
// in case all tabs have been removed on startup. No need to
// this if we have a known session to display.
observeRestoreComplete(requireComponents.core.store, findNavController())
}
observeTabSelection(requireComponents.core.store)
observeTabSelection(requireComponents.core.store)
if (!onboarding.userHasBeenOnboarded()) {
observeTabSource(requireComponents.core.store)
}
if (!onboarding.userHasBeenOnboarded()) {
observeTabSource(requireComponents.core.store)
}
requireContext().accessibilityManager.addAccessibilityStateChangeListener(this)
Unit
}
requireContext().accessibilityManager.addAccessibilityStateChangeListener(this)
Unit
}
private fun initializeUI(view: View) {
val tab = getCurrentTab()
@ -266,7 +269,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
}
}
@Suppress("ComplexMethod", "LongMethod")
@Suppress("ComplexMethod", "LongMethod", "DEPRECATION")
// https://github.com/mozilla-mobile/fenix/issues/19920
@CallSuper
internal open fun initializeUI(view: View, tab: SessionState) {
val context = requireContext()
@ -309,7 +313,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
thumbnailsFeature.get()?.requestScreenshot()
findNavController().nav(
R.id.browserFragment,
getTrayDirection(context)
BrowserFragmentDirections.actionGlobalTabsTrayFragment()
)
},
onCloseTab = { closedSession ->
@ -357,7 +361,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
browserStore = store
)
_browserInteractor = BrowserInteractor(
_browserToolbarInteractor = DefaultBrowserToolbarInteractor(
browserToolbarController,
browserToolbarMenuController
)
@ -365,7 +369,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
_browserToolbarView = BrowserToolbarView(
container = view.browserLayout,
toolbarPosition = context.settings().toolbarPosition,
interactor = browserInteractor,
interactor = browserToolbarInteractor,
customTabSession = customTabSessionId?.let { store.state.findCustomTab(it) },
lifecycleOwner = viewLifecycleOwner
)
@ -420,7 +424,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
window = requireActivity().window,
store = store,
customTabId = customTabSessionId,
isSecure = { !allowScreenshotsInPrivateMode && it.content.private }
isSecure = { !allowScreenshotsInPrivateMode && it.content.private },
clearFlagOnStop = false
),
owner = this,
view = view
@ -489,16 +494,14 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
val dynamicDownloadDialog = DynamicDownloadDialog(
container = view.browserLayout,
downloadState = downloadState,
metrics = requireComponents.analytics.metrics,
didFail = downloadJobStatus == DownloadState.Status.FAILED,
tryAgain = downloadFeature::tryAgain,
onCannotOpenFile = {
showCannotOpenFileError(view.browserLayout, context, it)
},
view = view.viewDynamicDownloadDialog,
toolbarHeight = toolbarHeight,
onDismiss = { sharedViewModel.downloadDialogState.remove(downloadState.sessionId) }
)
toolbarHeight = toolbarHeight
) { sharedViewModel.downloadDialogState.remove(downloadState.sessionId) }
dynamicDownloadDialog.show()
browserToolbarView.expand()
@ -585,7 +588,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
showPage = true,
sessionId = getCurrentTab()?.id
)
findNavController().navigateBlockingForAsyncNavGraph(directions)
findNavController().navigate(directions)
}
},
onNeedToRequestPermissions = { permissions ->
@ -596,14 +599,14 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
browserAnimator.captureEngineViewAndDrawStatically {
val directions =
NavGraphDirections.actionGlobalSavedLoginsAuthFragment()
findNavController().navigateBlockingForAsyncNavGraph(directions)
findNavController().navigate(directions)
}
},
creditCardPickerView = creditCardSelectBar,
onManageCreditCards = {
val directions =
NavGraphDirections.actionGlobalCreditCardsSettingFragment()
findNavController().navigateBlockingForAsyncNavGraph(directions)
findNavController().navigate(directions)
},
onSelectCreditCard = {
showBiometricPrompt(context)
@ -650,7 +653,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
sitePermissionsFeature.set(
feature = SitePermissionsFeature(
context = context,
storage = context.components.core.permissionStorage.permissionsStorage,
storage = context.components.core.geckoSitePermissionsStorage,
fragmentManager = parentFragmentManager,
promptsStyling = SitePermissionsFeature.PromptsStyling(
gravity = getAppropriateLayoutGravity(),
@ -822,15 +825,17 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
internal fun expandToolbarOnNavigation(store: BrowserStore) {
consumeFlow(store) { flow ->
flow.mapNotNull {
state -> state.findCustomTabOrSelectedTab(customTabSessionId)
}
.ifAnyChanged {
tab -> arrayOf(tab.content.url, tab.content.loadRequest)
}
.collect {
findInPageIntegration.onBackPressed()
browserToolbarView.expand()
state ->
state.findCustomTabOrSelectedTab(customTabSessionId)
}
.ifAnyChanged {
tab ->
arrayOf(tab.content.url, tab.content.loadRequest)
}
.collect {
findInPageIntegration.onBackPressed()
browserToolbarView.expand()
}
}
}
@ -889,7 +894,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
DynamicDownloadDialog(
container = view.browserLayout,
downloadState = savedDownloadState.first,
metrics = requireComponents.analytics.metrics,
didFail = savedDownloadState.second,
tryAgain = onTryAgain,
onCannotOpenFile = {
@ -906,8 +910,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
@VisibleForTesting
internal fun shouldPullToRefreshBeEnabled(inFullScreen: Boolean): Boolean {
return FeatureFlags.pullToRefreshEnabled &&
requireContext().settings().isPullToRefreshEnabledInBrowser &&
!inFullScreen
requireContext().settings().isPullToRefreshEnabledInBrowser &&
!inFullScreen
}
@VisibleForTesting
@ -980,12 +984,12 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
flow.ifChanged {
it.selectedTabId
}
.mapNotNull {
it.selectedTab
}
.collect {
handleTabSelected(it)
}
.mapNotNull {
it.selectedTab
}
.collect {
handleTabSelected(it)
}
}
}
@ -997,14 +1001,14 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
state.selectedTab
}
.collect {
if (!onboarding.userHasBeenOnboarded() &&
it.content.loadRequest?.triggeredByRedirect != true &&
it.source !in intentSourcesList &&
it.content.url !in onboardingLinksList
) {
onboarding.finish()
if (!onboarding.userHasBeenOnboarded() &&
it.content.loadRequest?.triggeredByRedirect != true &&
it.source !is SessionState.Source.External &&
it.content.url !in onboardingLinksList
) {
onboarding.finish()
}
}
}
}
}
@ -1069,14 +1073,14 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
@CallSuper
override fun onBackPressed(): Boolean {
return findInPageIntegration.onBackPressed() ||
fullScreenFeature.onBackPressed() ||
promptsFeature.onBackPressed() ||
sessionFeature.onBackPressed() ||
removeSessionIfNeeded()
fullScreenFeature.onBackPressed() ||
promptsFeature.onBackPressed() ||
sessionFeature.onBackPressed() ||
removeSessionIfNeeded()
}
override fun onBackLongPressed(): Boolean {
findNavController().navigateBlockingForAsyncNavGraph(
findNavController().navigate(
NavGraphDirections.actionGlobalTabHistoryDialogFragment(
activeSessionId = customTabSessionId
)
@ -1137,7 +1141,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
*/
protected open fun removeSessionIfNeeded(): Boolean {
getCurrentTab()?.let { session ->
return if (session.source == SessionState.Source.ACTION_VIEW) {
return if (session.source is SessionState.Source.External && !session.restored) {
activity?.finish()
requireComponents.useCases.tabsUseCases.removeTab(session.id)
true
@ -1186,7 +1190,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
val tab = getCurrentTab() ?: return
viewLifecycleOwner.lifecycleScope.launch(Main) {
val sitePermissions: SitePermissions? = tab.content.url.getOrigin()?.let { origin ->
val storage = requireComponents.core.permissionStorage
val storage = requireComponents.core.permissionStorage
storage.findSitePermissionsBy(origin)
}
@ -1343,7 +1347,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
requireContext().accessibilityManager.removeAccessibilityStateChangeListener(this)
_browserToolbarView = null
_browserInteractor = null
_browserToolbarInteractor = null
}
override fun onAttach(context: Context) {
@ -1379,17 +1383,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
.show()
}
/**
* Retrieves the correct tray direction while using a feature flag.
*
* Remove this when [FeatureFlags.tabsTrayRewrite] is removed.
*/
private fun getTrayDirection(context: Context) = if (context.settings().tabsTrayRewrite) {
BrowserFragmentDirections.actionGlobalTabsTrayFragment()
} else {
BrowserFragmentDirections.actionGlobalTabTrayDialogFragment()
}
companion object {
private const val KEY_CUSTOM_TAB_SESSION_ID = "custom_tab_session_id"
private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 1
@ -1400,12 +1393,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.PRIVATE_NOTICE),
SupportUtils.getFirefoxAccountSumoUrl()
)
val intentSourcesList: List<SessionState.Source> = listOf(
SessionState.Source.ACTION_SEARCH,
SessionState.Source.ACTION_SEND,
SessionState.Source.ACTION_VIEW
)
}
override fun onAccessibilityStateChanged(enabled: Boolean) {

@ -8,6 +8,7 @@ import android.content.Context
import android.os.StrictMode
import android.view.View
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.appcompat.content.res.AppCompatResources
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
@ -16,29 +17,31 @@ import kotlinx.android.synthetic.main.fragment_browser.*
import kotlinx.android.synthetic.main.fragment_browser.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.thumbnails.BrowserThumbnails
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.concept.engine.permission.SitePermissions
import mozilla.components.feature.app.links.AppLinksUseCases
import mozilla.components.feature.contextmenu.ContextMenuCandidate
import mozilla.components.feature.readerview.ReaderViewFeature
import mozilla.components.feature.sitepermissions.SitePermissions
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tabs.WindowFeature
import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.shortcut.PwaOnboardingObserver
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.trackingprotection.TrackingProtectionOverlay
/**
@ -50,7 +53,8 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
private val windowFeature = ViewBoundFeatureWrapper<WindowFeature>()
private val openInAppOnboardingObserver = ViewBoundFeatureWrapper<OpenInAppOnboardingObserver>()
private val trackingProtectionOverlayObserver = ViewBoundFeatureWrapper<TrackingProtectionOverlay>()
private val trackingProtectionOverlayObserver =
ViewBoundFeatureWrapper<TrackingProtectionOverlay>()
private var readerModeAvailable = false
private var pwaOnboardingObserver: PwaOnboardingObserver? = null
@ -75,20 +79,40 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
)
}
if (FeatureFlags.showHomeButtonFeature) {
val homeAction = BrowserToolbar.Button(
imageDrawable = AppCompatResources.getDrawable(
requireContext(),
R.drawable.mozac_ic_home
)!!,
contentDescription = requireContext().getString(R.string.browser_toolbar_home),
iconTintColorResource = ThemeManager.resolveAttribute(R.attr.primaryText, context),
listener = browserToolbarInteractor::onHomeButtonClicked
)
browserToolbarView.view.addNavigationAction(homeAction)
}
val readerModeAction =
BrowserToolbar.ToggleButton(
image = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_readermode)!!,
image = AppCompatResources.getDrawable(
requireContext(),
R.drawable.ic_readermode
)!!,
imageSelected =
AppCompatResources.getDrawable(requireContext(), R.drawable.ic_readermode_selected)!!,
AppCompatResources.getDrawable(
requireContext(),
R.drawable.ic_readermode_selected
)!!,
contentDescription = requireContext().getString(R.string.browser_menu_read),
contentDescriptionSelected = requireContext().getString(R.string.browser_menu_read_close),
visible = {
readerModeAvailable
},
selected = getCurrentTab()?.let {
activity?.components?.core?.store?.state?.findTab(it.id)?.readerState?.active
} ?: false,
listener = browserInteractor::onReaderModePressed
activity?.components?.core?.store?.state?.findTab(it.id)?.readerState?.active
} ?: false,
listener = browserToolbarInteractor::onReaderModePressed
)
browserToolbarView.view.addPageAction(readerModeAction)
@ -183,10 +207,21 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
override fun onStop() {
super.onStop()
updateLastBrowseActivity()
if (requireContext().settings().historyMetadataFeature) {
updateHistoryMetadata()
}
pwaOnboardingObserver?.stop()
}
private fun updateHistoryMetadata() {
getCurrentTab()?.let { tab ->
(tab as? TabSessionState)?.historyMetadata?.let {
requireComponents.core.historyMetadataService.updateMetadata(it, tab)
}
}
}
private fun subscribeToTabCollections() {
Observer<List<TabCollection>> {
requireComponents.core.tabCollectionStorage.cachedTabCollections = it
@ -222,22 +257,27 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
override fun navToTrackingProtectionPanel(tab: SessionState) {
val navController = findNavController()
requireComponents.useCases.trackingProtectionUseCases.containsException(tab.id) { contains ->
val isEnabled = tab.trackingProtection.enabled && !contains
val directions =
BrowserFragmentDirections.actionBrowserFragmentToTrackingProtectionPanelDialogFragment(
sessionId = tab.id,
url = tab.content.url,
trackingProtectionEnabled = isEnabled,
gravity = getAppropriateLayoutGravity()
)
navController.navigateSafe(R.id.browserFragment, directions)
runIfFragmentIsAttached {
val isEnabled = tab.trackingProtection.enabled && !contains
val directions =
BrowserFragmentDirections.actionBrowserFragmentToTrackingProtectionPanelDialogFragment(
sessionId = tab.id,
url = tab.content.url,
trackingProtectionEnabled = isEnabled,
gravity = getAppropriateLayoutGravity()
)
navController.navigateSafe(R.id.browserFragment, directions)
}
}
}
private val collectionStorageObserver = object : TabCollectionStorage.Observer {
override fun onCollectionCreated(title: String, sessions: List<TabSessionState>, id: Long?) {
override fun onCollectionCreated(
title: String,
sessions: List<TabSessionState>,
id: Long?
) {
showTabSavedToCollectionSnackbar(sessions.size, true)
}
@ -245,7 +285,10 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
showTabSavedToCollectionSnackbar(sessions.size)
}
private fun showTabSavedToCollectionSnackbar(tabSize: Int, isNewCollection: Boolean = false) {
private fun showTabSavedToCollectionSnackbar(
tabSize: Int,
isNewCollection: Boolean = false
) {
view?.let { view ->
val messageStringRes = when {
isNewCollection -> {
@ -265,7 +308,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
)
.setText(view.context.getString(messageStringRes))
.setAction(requireContext().getString(R.string.create_collection_view)) {
findNavController().navigateBlockingForAsyncNavGraph(
findNavController().navigate(
BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = false)
)
}
@ -289,7 +332,19 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
context.components.useCases.contextMenuUseCases,
view,
FenixSnackbarDelegate(view)
) + ContextMenuCandidate.createOpenInExternalAppCandidate(requireContext(),
contextMenuCandidateAppLinksUseCases)
) + ContextMenuCandidate.createOpenInExternalAppCandidate(
requireContext(),
contextMenuCandidateAppLinksUseCases
)
}
/**
* Updates the last time the user was active on the [BrowserFragment].
* This is useful to determine if the user has to start on the [HomeFragment]
* or it should go directly to the [BrowserFragment].
*/
@VisibleForTesting
internal fun updateLastBrowseActivity() {
requireContext().settings().lastBrowseActivity = System.currentTimeMillis()
}
}

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

Loading…
Cancel
Save