Merge tag 'v92.1.1' into upstream-sync

pull/393/head
Adam Novak 3 years ago
commit 7e870605d8

@ -50,4 +50,8 @@
# Possible regressions throughout the app # Possible regressions throughout the app
*.pro @mozilla-mobile/Performance *.pro @mozilla-mobile/Performance
*proguard* @mozilla-mobile/Performance *proguard* @mozilla-mobile/Performance
# This file configures perf tests via Jetpack Benchmark.
app/benchmark.gradle @mozilla-mobile/Performance
# --- PERFORMANCE END --- # # --- 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 ## Sprint X.1 End [Wednesday, 2nd week] Cutting a Beta
- [ ] Make a new 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. - ⚠️ 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 - [ ] 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. - [ ] 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)) - [ ] 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) ### 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). - [ ] 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. - [ ] 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) - [ ] 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] ### 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). - [ ] 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) - [ ] 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/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) - [ ] 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. - [ ] 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 - [ ] 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: exemptLabels:
- pin - pin
- "feature request 🌟" - "feature request 🌟"
- "eng:disabled-test"
# Set to true to ignore issues in a project (defaults to false) # Set to true to ignore issues in a project (defaults to false)
exemptProjects: false exemptProjects: false

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

@ -15,12 +15,15 @@ jobs:
steps: steps:
- name: "Discover Fenix Beta Version" - name: "Discover Fenix Beta Version"
id: 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" - name: "Checkout Master Branch"
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
path: main path: main
ref: master ref: main
- name: "Checkout Beta Branch" - name: "Checkout Beta Branch"
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
@ -37,5 +40,6 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
path: beta path: beta
branch: automation/sync-strings-${{ steps.fenix-beta-version.outputs.major-beta-version }} 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" title: "Sync Strings from main 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`" 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 strict: smart
delete_head_branch: delete_head_branch:
force: false 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: - $let:
taskgraph: taskgraph:
branch: taskgraph branch: taskgraph
revision: a458418ef7cdd6778f1283926c6116966255bc24 revision: 9daff451cfbe82c5c70237a7b3dbcf4fd3238299
trustDomain: mobile trustDomain: mobile
in: in:
$let: $let:
@ -243,7 +243,7 @@ tasks:
# Note: This task is built server side without the context or tooling that # Note: This task is built server side without the context or tooling that
# exist in tree so we must hard code the hash # exist in tree so we must hard code the hash
image: image:
mozillareleases/taskgraph:decision-mobile-682fbaa1ef17e70ddfe3457da3eaf8e776c4a20fe5bfbdbeba0641fd5bceae2a@sha256:bbb2613aaab79d17e590fbd78c072d0643be40fd1237195703f84280ecc3b302 mozillareleases/taskgraph:decision-mobile-44b6b7b4c370220eff56efa8b508aa5157ef9c6e74847c7ecc19d640946ba49e@sha256:4107cbc5e154502529e4d38efa4dc89c05ee54e2cbc6e2e66023e68407502894
maxRunTime: 1800 maxRunTime: 1800
@ -260,14 +260,14 @@ tasks:
in: in:
$if: 'tasks_for == "action"' $if: 'tasks_for == "action"'
then: > then: >
PIP_IGNORE_INSTALLED=0 pip install --user /builds/worker/checkouts/taskgraph && PIP_IGNORE_INSTALLED=0 pip3 install --user /builds/worker/checkouts/taskgraph &&
PIP_IGNORE_INSTALLED=0 pip install --user mozilla-version && PIP_IGNORE_INSTALLED=0 pip3 install --user mozilla-version &&
taskcluster/scripts/decision-install-sdk.sh && taskcluster/scripts/decision-install-sdk.sh &&
ln -s /builds/worker/artifacts artifacts && ln -s /builds/worker/artifacts artifacts &&
~/.local/bin/taskgraph action-callback ~/.local/bin/taskgraph action-callback
else: > else: >
PIP_IGNORE_INSTALLED=0 pip install --user /builds/worker/checkouts/taskgraph && PIP_IGNORE_INSTALLED=0 pip3 install --user /builds/worker/checkouts/taskgraph &&
PIP_IGNORE_INSTALLED=0 pip install --user mozilla-version && PIP_IGNORE_INSTALLED=0 pip3 install --user mozilla-version &&
taskcluster/scripts/decision-install-sdk.sh && taskcluster/scripts/decision-install-sdk.sh &&
ln -s /builds/worker/artifacts artifacts && ln -s /builds/worker/artifacts artifacts &&
~/.local/bin/taskgraph decision ~/.local/bin/taskgraph decision

8
Jenkinsfile vendored

@ -1,7 +1,7 @@
pipeline { pipeline {
agent any agent any
triggers { triggers {
cron(env.BRANCH_NAME == 'master' ? 'H 0 * * *' : '') cron(env.BRANCH_NAME == 'main' ? 'H 0 * * *' : '')
} }
options { options {
timestamps() timestamps()
@ -9,7 +9,7 @@ pipeline {
} }
stages { stages {
stage('test') { stage('test') {
when { branch 'master' } when { branch 'main' }
steps { steps {
dir('app/src/androidTest/java/org/mozilla/fenix/syncIntegration') { dir('app/src/androidTest/java/org/mozilla/fenix/syncIntegration') {
sh 'pipenv install' sh 'pipenv install'
@ -22,7 +22,7 @@ pipeline {
post { post {
always { always {
script { script {
if (env.BRANCH_NAME == 'master') { if (env.BRANCH_NAME == 'main') {
publishHTML(target: [ publishHTML(target: [
allowMissing: false, allowMissing: false,
alwaysLinkToLastBuild: true, alwaysLinkToLastBuild: true,
@ -36,7 +36,7 @@ pipeline {
failure { failure {
script { script {
if (env.BRANCH_NAME == 'master') { if (env.BRANCH_NAME == 'main') {
slackSend( slackSend(
color: 'danger', color: 'danger',
message: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL}HTML_20Report/)") 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: 'jacoco'
apply plugin: 'androidx.navigation.safeargs.kotlin' apply plugin: 'androidx.navigation.safeargs.kotlin'
apply plugin: 'com.google.android.gms.oss-licenses-plugin' apply plugin: 'com.google.android.gms.oss-licenses-plugin'
apply plugin: 'androidx.benchmark'
import com.android.build.OutputFile 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 import static org.gradle.api.tasks.testing.TestResult.ResultType
apply from: 'benchmark.gradle'
android { android {
compileSdkVersion Config.compileSdkVersion compileSdkVersion Config.compileSdkVersion
project.maybeConfigForJetpackBenchmark(it)
if (project.hasProperty("testBuildType")) { if (project.hasProperty("testBuildType")) {
// Allowing to configure the test build type via command line flag (./gradlew -PtestBuildType=beta ..) // 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. // 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. // in automation for UI testing non-debug builds.
shrinkResources !project.hasProperty("disableOptimization") shrinkResources !project.hasProperty("disableOptimization")
minifyEnabled !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) matchingFallbacks = ['release'] // Use on the "release" build type in dependencies (AARs)
if (gradle.hasProperty("localProperties.autosignReleaseWithDebugKey")) { if (gradle.hasProperty("localProperties.autosignReleaseWithDebugKey")) {
@ -174,6 +178,10 @@ android {
} }
buildFeatures {
viewBinding true
}
aaptOptions { aaptOptions {
// All JavaScript code used internally by GeckoView is packaged in a // All JavaScript code used internally by GeckoView is packaged in a
// file called omni.ja. If this file is compressed in the APK, // file called omni.ja. If this file is compressed in the APK,
@ -200,7 +208,6 @@ android {
} }
beta { beta {
java.srcDirs = ['src/migration/java'] java.srcDirs = ['src/migration/java']
manifest.srcFile "src/migration/AndroidManifest.xml"
} }
release { release {
java.srcDirs = ['src/migration/java'] java.srcDirs = ['src/migration/java']
@ -253,6 +260,14 @@ android {
minHeapSize = "1024m" minHeapSize = "1024m"
} }
} }
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion = Versions.androidx_compose
}
} }
android.applicationVariants.all { variant -> android.applicationVariants.all { variant ->
@ -293,12 +308,15 @@ android.applicationVariants.all { variant ->
println("versionName override: $versionName") println("versionName override: $versionName")
variant.outputs.each { output -> variant.outputs.each { output ->
def isMozillaOnline = project.hasProperty("mozillaOnline") || gradle.hasProperty("localProperties.mozillaOnline")
def abi = output.getFilter(OutputFile.ABI) 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 // We use the same version code generator, that we inherited from Fennec, across all channels - even on
// channels that never shipped a Fennec build. // 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.versionNameOverride = versionName
output.versionCodeOverride = versionCodeOverride output.versionCodeOverride = versionCodeOverride
@ -428,10 +446,6 @@ configurations {
// correct runtime classpath when invoked with Android Studio's built-in JUnit test runner. // correct runtime classpath when invoked with Android Studio's built-in JUnit test runner.
// Success! // Success!
jnaForTest 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 { dependencies {
@ -465,7 +479,6 @@ dependencies {
implementation Deps.mozilla_browser_icons implementation Deps.mozilla_browser_icons
implementation Deps.mozilla_browser_menu implementation Deps.mozilla_browser_menu
implementation Deps.mozilla_browser_menu2 implementation Deps.mozilla_browser_menu2
implementation Deps.mozilla_browser_session
implementation Deps.mozilla_browser_session_storage implementation Deps.mozilla_browser_session_storage
implementation Deps.mozilla_browser_state implementation Deps.mozilla_browser_state
implementation Deps.mozilla_browser_storage_sync implementation Deps.mozilla_browser_storage_sync
@ -538,6 +551,10 @@ dependencies {
debugImplementation Deps.leakcanary debugImplementation Deps.leakcanary
forkDebugImplementation 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_legacy
implementation Deps.androidx_biometric implementation Deps.androidx_biometric
implementation Deps.androidx_paging implementation Deps.androidx_paging
@ -588,6 +605,7 @@ dependencies {
androidTestImplementation Deps.androidx_junit androidTestImplementation Deps.androidx_junit
androidTestImplementation Deps.androidx_work_testing androidTestImplementation Deps.androidx_work_testing
androidTestImplementation Deps.androidx_benchmark_junit4
androidTestImplementation Deps.mockwebserver androidTestImplementation Deps.mockwebserver
testImplementation Deps.mozilla_support_test testImplementation Deps.mozilla_support_test
testImplementation Deps.mozilla_support_test_libstate testImplementation Deps.mozilla_support_test_libstate
@ -612,6 +630,11 @@ dependencies {
if (project.hasProperty("coverage")) { if (project.hasProperty("coverage")) {
tasks.withType(Test).configureEach { tasks.withType(Test).configureEach {
jacoco.includeNoLocationClasses = true jacoco.includeNoLocationClasses = true
jacoco.excludes = ['jdk.internal.*']
}
jacoco {
toolVersion = "0.8.7"
} }
android.applicationVariants.all { variant -> android.applicationVariants.all { variant ->
@ -779,14 +802,3 @@ ext.updateExtensionVersion = { task, extDir ->
expand(values) 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. --> <!-- Performance: checks we'd like to eventually set to error. -->
<issue id="UseCompoundDrawables" severity="warning" /> <issue id="UseCompoundDrawables" severity="warning" />
<issue id="Overdraw" 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. --> <!-- Performance: checks that we're unsure of the value of that we might want to investigate. -->
<issue id="UnpackedNativeCode" severity="informational" /> <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 { *; } -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 { -assumenosideeffects class kotlinx.coroutines.internal.MainDispatcherLoader {
boolean FAST_SERVICE_LOADER_ENABLED return false; boolean FAST_SERVICE_LOADER_ENABLED return true;
}
-assumenosideeffects class kotlinx.coroutines.internal.FastServiceLoader {
boolean ANDROID_DETECTED return true;
} }
#################################################################################################### ####################################################################################################

@ -9,7 +9,7 @@ import androidx.navigation.NavController
import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.request.RequestInterceptor import mozilla.components.concept.engine.request.RequestInterceptor
import org.mozilla.fenix.ext.components 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 import java.lang.ref.WeakReference
/** /**

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

@ -11,8 +11,8 @@ import androidx.test.rule.ActivityTestRule
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.UiSelector
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.helpers.TestHelper.appContext
import org.mozilla.fenix.onboarding.FenixOnboarding 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]. * 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.UiSelector
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import mozilla.components.support.ktx.android.content.appName
import org.hamcrest.CoreMatchers import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.mozilla.fenix.R import org.mozilla.fenix.R
@ -41,10 +42,13 @@ import java.io.File
object TestHelper { 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 { fun scrollToElementByText(text: String): UiScrollable {
val appView = UiScrollable(UiSelector().scrollable(true)) val appView = UiScrollable(UiSelector().scrollable(true))
appView.waitForExists(waitingTime)
appView.scrollTextIntoView(text) appView.scrollTextIntoView(text)
return appView return appView
} }
@ -69,7 +73,7 @@ object TestHelper {
editor.apply() editor.apply()
} }
fun restartApp(activity: HomeActivityTestRule) { fun restartApp(activity: HomeActivityIntentTestRule) {
with(activity) { with(activity) {
finishActivity() finishActivity()
mDevice.waitForIdle() mDevice.waitForIdle()
@ -79,7 +83,7 @@ object TestHelper {
fun getPermissionAllowID(): String { fun getPermissionAllowID(): String {
return when return when
(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
true -> "com.android.permissioncontroller" true -> "com.android.permissioncontroller"
false -> "com.android.packageinstaller" false -> "com.android.packageinstaller"
} }

@ -16,10 +16,10 @@ import java.net.URISyntaxException
*/ */
fun String.removePrefixesIgnoreCase(vararg prefixes: String): String { fun String.removePrefixesIgnoreCase(vararg prefixes: String): String {
var value = this var value = this
var lower = this.toLowerCase() var lower = this.lowercase()
prefixes.forEach { prefixes.forEach {
if (lower.startsWith(it.toLowerCase())) { if (lower.startsWith(it.lowercase())) {
value = value.substring(it.length) value = value.substring(it.length)
lower = lower.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( private val failureMsgNumberOfInflation = getErrorMessage(
shortName = "Number of inflation on start up doesn't match expected count", 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" + 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 * 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) { return if (view !is ViewGroup) {
viewValue viewValue
} else { } else {
viewValue + view.children.sumBy { countRecyclerViewConstraintLayoutChildren(it, view) } viewValue + view.children.sumOf { countRecyclerViewConstraintLayoutChildren(it, view) }
} }
} }

@ -83,14 +83,14 @@ class MenuScreenShotTest : ScreenshotTest() {
}.openLanguageSubMenu { }.openLanguageSubMenu {
Screengrab.screenshot("SettingsSubMenuAccessibilityRobot_settings-language") Screengrab.screenshot("SettingsSubMenuAccessibilityRobot_settings-language")
}.goBack { }.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 { }.openSetDefaultBrowserSubMenu {
Screengrab.screenshot("SettingsSubMenuDefaultBrowserRobot_settings-default-browser") Screengrab.screenshot("SettingsSubMenuDefaultBrowserRobot_settings-default-browser")
}.goBack { }.goBack {
// Disabled for Pixel 2 // Disabled for Pixel 2
// }.openEnhancedTrackingProtectionSubMenu { // }.openEnhancedTrackingProtectionSubMenu {
// Screengrab.screenshot("settings-enhanced-tp") // Screengrab.screenshot("settings-enhanced-tp")
// }.goBack { // }.goBack {
}.openLoginsAndPasswordSubMenu { }.openLoginsAndPasswordSubMenu {
Screengrab.screenshot("SettingsSubMenuLoginsAndPasswords-settings-logins-passwords") Screengrab.screenshot("SettingsSubMenuLoginsAndPasswords-settings-logins-passwords")
}.goBack { }.goBack {
@ -176,7 +176,7 @@ class MenuScreenShotTest : ScreenshotTest() {
@Test @Test
fun saveLoginPromptTest() { fun saveLoginPromptTest() {
val saveLoginTest = val saveLoginTest =
TestAssetHelper.getSaveLoginAsset(mockWebServer) TestAssetHelper.getSaveLoginAsset(mockWebServer)
navigationToolbar { navigationToolbar {
}.enterURLAndEnterToBrowser(saveLoginTest.url) { }.enterURLAndEnterToBrowser(saveLoginTest.url) {
verifySaveLoginPromptIsShownNotSave() verifySaveLoginPromptIsShownNotSave()

@ -173,9 +173,11 @@ class SyncIntegrationTest {
// Useful functions for the tests // Useful functions for the tests
fun typeEmail() { fun typeEmail() {
val emailInput = mDevice.findObject(UiSelector() val emailInput = mDevice.findObject(
UiSelector()
.instance(0) .instance(0)
.className(EditText::class.java)) .className(EditText::class.java)
)
emailInput.waitForExists(TestAssetHelper.waitingTime) emailInput.waitForExists(TestAssetHelper.waitingTime)
val emailAddress = javaClass.classLoader!!.getResource("email.txt").readText() val emailAddress = javaClass.classLoader!!.getResource("email.txt").readText()
@ -188,9 +190,11 @@ class SyncIntegrationTest {
} }
fun typePassword() { fun typePassword() {
val passwordInput = mDevice.findObject(UiSelector() val passwordInput = mDevice.findObject(
UiSelector()
.instance(0) .instance(0)
.className(EditText::class.java)) .className(EditText::class.java)
)
val passwordValue = javaClass.classLoader!!.getResource("password.txt").readText() val passwordValue = javaClass.classLoader!!.getResource("password.txt").readText()
passwordInput.setText(passwordValue) 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() { fun openSettingsLogins() {
robot.openSettingsLogins { robot.openSettingsLogins {
verifyDefaultView() verifyDefaultView()
verifyDefaultValueAutofillLogins() verifyDefaultValueAutofillLogins(InstrumentationRegistry.getInstrumentation().targetContext)
} }
} }

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

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

@ -34,6 +34,8 @@ class NoNetworkAccessStartupTests {
} }
@Test @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 // Based on STR from https://github.com/mozilla-mobile/fenix/issues/16886
fun noNetworkConnectionStartupTest() { fun noNetworkConnectionStartupTest() {
setNetworkEnabled(false) setNetworkEnabled(false)

@ -96,7 +96,7 @@ class SettingsAddonsTest {
acceptInstallAddon() acceptInstallAddon()
verifyDownloadAddonPrompt(addonName, activityTestRule) verifyDownloadAddonPrompt(addonName, activityTestRule)
} }
} }
// Opens the addons settings menu, installs an addon, then uninstalls // 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 @Test
fun changeAccessibiltySettings() { fun changeAccessibiltySettings() {
// Goes through the settings and changes the default text on a webpage, then verifies if the text has changed. // 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() 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.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher 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.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.openAppFromExternalLink import org.mozilla.fenix.helpers.TestHelper.openAppFromExternalLink
@ -36,7 +36,7 @@ class SettingsPrivacyTest {
private val pageShortcutName = "TestShortcut" private val pageShortcutName = "TestShortcut"
@get:Rule @get:Rule
val activityTestRule = HomeActivityTestRule() val activityTestRule = HomeActivityIntentTestRule()
@Before @Before
fun setUp() { fun setUp() {
@ -176,7 +176,7 @@ class SettingsPrivacyTest {
TestHelper.scrollToElementByText("Logins and passwords") TestHelper.scrollToElementByText("Logins and passwords")
}.openLoginsAndPasswordSubMenu { }.openLoginsAndPasswordSubMenu {
verifyDefaultView() verifyDefaultView()
verifyDefaultValueAutofillLogins() verifyDefaultValueAutofillLogins(InstrumentationRegistry.getInstrumentation().targetContext)
verifyDefaultValueExceptions() verifyDefaultValueExceptions()
}.openSavedLogins { }.openSavedLogins {
verifySecurityPromptForLogins() verifySecurityPromptForLogins()
@ -270,22 +270,24 @@ class SettingsPrivacyTest {
} }
@Test @Test
@Ignore("See: https://github.com/mozilla-mobile/fenix/issues/10915")
fun openExternalLinksInPrivateTest() { fun openExternalLinksInPrivateTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
setOpenLinksInPrivateOn() setOpenLinksInPrivateOn()
openAppFromExternalLink(defaultWebPage.url.toString()) openAppFromExternalLink(firstWebPage.url.toString())
browserScreen { browserScreen {
}.openTabDrawer { }.openTabDrawer {
verifyPrivateModeSelected() verifyPrivateModeSelected()
}.openNewTab { }.dismissSearchBar { } }.closeTabDrawer {
}.goToHomescreen { }
setOpenLinksInPrivateOff() setOpenLinksInPrivateOff()
openAppFromExternalLink(defaultWebPage.url.toString()) // We need to open a different link, otherwise it will open the same session
openAppFromExternalLink(secondWebPage.url.toString())
browserScreen { browserScreen {
}.openTabDrawer { }.openTabDrawer {
@ -294,7 +296,6 @@ class SettingsPrivacyTest {
} }
@Test @Test
@Ignore("See: https://github.com/mozilla-mobile/fenix/issues/10915")
fun launchPageShortcutInPrivateModeTest() { fun launchPageShortcutInPrivateModeTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -307,7 +308,18 @@ class SettingsPrivacyTest {
addShortcutName(pageShortcutName) addShortcutName(pageShortcutName)
clickAddShortcutButton() clickAddShortcutButton()
clickAddAutomaticallyButton() 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 { }.openTabDrawer {
verifyPrivateModeSelected() verifyPrivateModeSelected()
} }
@ -328,8 +340,7 @@ class SettingsPrivacyTest {
clickAddShortcutButton() clickAddShortcutButton()
clickAddAutomaticallyButton() clickAddAutomaticallyButton()
}.openHomeScreenShortcut(pageShortcutName) { }.openHomeScreenShortcut(pageShortcutName) {
}.openTabDrawer { }.goToHomescreen { }
}.openNewTab { }.dismissSearchBar { }
setOpenLinksInPrivateOff() setOpenLinksInPrivateOff()
restartApp(activityTestRule) restartApp(activityTestRule)
@ -339,8 +350,7 @@ class SettingsPrivacyTest {
}.searchAndOpenHomeScreenShortcut(pageShortcutName) { }.searchAndOpenHomeScreenShortcut(pageShortcutName) {
}.openTabDrawer { }.openTabDrawer {
verifyNormalModeSelected() verifyNormalModeSelected()
}.openNewTab { }.closeTabDrawer {
}.dismissSearchBar {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openSettings { }.openSettings {
}.openPrivateBrowsingSubMenu { }.openPrivateBrowsingSubMenu {

@ -20,23 +20,25 @@ import org.junit.Before
import org.junit.Ignore import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.IntentReceiverActivity import org.mozilla.fenix.IntentReceiverActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher 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.RecyclerViewIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper 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.createCustomTabIntent
import org.mozilla.fenix.helpers.TestHelper.deleteDownloadFromStorage 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.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource
import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.clickTabCrashedRestoreButton import org.mozilla.fenix.ui.robots.clickTabCrashedRestoreButton
import org.mozilla.fenix.ui.robots.clickUrlbar 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.customTabScreen
import org.mozilla.fenix.ui.robots.dismissTrackingOnboarding import org.mozilla.fenix.ui.robots.dismissTrackingOnboarding
import org.mozilla.fenix.ui.robots.downloadRobot 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.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.notificationShade 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.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 import org.mozilla.fenix.ui.util.STRING_ONBOARDING_TRACKING_PROTECTION_HEADER
/** /**
@ -63,6 +70,7 @@ class SmokeTest {
private val downloadFileName = "Globe.svg" private val downloadFileName = "Globe.svg"
private val collectionName = "First Collection" private val collectionName = "First Collection"
private var bookmarksListIdlingResource: RecyclerViewIdlingResource? = null private var bookmarksListIdlingResource: RecyclerViewIdlingResource? = null
private var localeListIdlingResource: RecyclerViewIdlingResource? = null
private val customMenuItem = "TestMenuItem" private val customMenuItem = "TestMenuItem"
// This finds the dialog fragment child of the homeFragment, otherwise the awesomeBar would return null // This finds the dialog fragment child of the homeFragment, otherwise the awesomeBar would return null
@ -75,7 +83,7 @@ class SmokeTest {
} }
@get:Rule @get:Rule
val activityTestRule = HomeActivityTestRule() val activityTestRule = HomeActivityIntentTestRule()
private lateinit var browserStore: BrowserStore private lateinit var browserStore: BrowserStore
@get: Rule @get: Rule
@ -130,6 +138,10 @@ class SmokeTest {
if (readerViewNotification != null) { if (readerViewNotification != null) {
IdlingRegistry.getInstance().unregister(readerViewNotification) IdlingRegistry.getInstance().unregister(readerViewNotification)
} }
if (localeListIdlingResource != null) {
IdlingRegistry.getInstance().unregister(localeListIdlingResource)
}
} }
// Verifies the first run onboarding screen // 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. // 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. // The test is assuming we are NOT signed in.
fun openMainMenuSyncItemTest() { fun openMainMenuSyncItemTest() {
if (FeatureFlags.tabsTrayRewrite) { homeScreen {
homeScreen { }.openThreeDotMenu {
}.openThreeDotMenu { }.openSyncSignIn {
}.openSyncSignIn { verifySyncSignInMenuHeader()
verifySyncSignInMenuHeader()
}
} else {
homeScreen {
}.openThreeDotMenu {
}.openSyncedTabs {
verifySyncedTabsMenuHeader()
}
} }
} }
// Could be removed when more smoke tests from the Settings category are added
@Test @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 // Verifies the Settings menu opens from a tab's 3 dot menu
fun openMainMenuSettingsItemTest() { fun openMainMenuSettingsItemTest() {
homeScreen { homeScreen {
@ -414,9 +419,9 @@ class SmokeTest {
} }
} }
@Ignore("Failing, see https://github.com/mozilla-mobile/fenix/issues/18647")
@Test @Test
fun customTrackingProtectionSettingsTest() { fun customTrackingProtectionSettingsTest() {
val genericWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val trackingPage = TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer) val trackingPage = TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer)
homeScreen { homeScreen {
@ -429,10 +434,14 @@ class SmokeTest {
}.goBackToHomeScreen {} }.goBackToHomeScreen {}
navigationToolbar { 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 { enhancedTrackingProtection {
dismissTrackingOnboarding()
}.openEnhancedTrackingProtectionSheet { }.openEnhancedTrackingProtectionSheet {
verifyTrackingCookiesBlocked() verifyTrackingCookiesBlocked()
verifyCryptominersBlocked() verifyCryptominersBlocked()
@ -511,6 +520,8 @@ class SmokeTest {
} }
@Test @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. // Goes through the settings and changes the search suggestion toggle, then verifies it changes.
fun toggleSearchSuggestions() { fun toggleSearchSuggestions() {
@ -564,6 +575,7 @@ class SmokeTest {
@Test @Test
// Saves a login, then changes it and verifies the update // 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() { fun updateSavedLoginTest() {
val saveLoginTest = val saveLoginTest =
TestAssetHelper.getSaveLoginAsset(mockWebServer) TestAssetHelper.getSaveLoginAsset(mockWebServer)
@ -590,7 +602,7 @@ class SmokeTest {
verifySavedLoginFromPrompt() verifySavedLoginFromPrompt()
viewSavedLoginDetails() viewSavedLoginDetails()
revealPassword() revealPassword()
verifyPasswordSaved("test") verifyPasswordSaved("test") // failing here locally
} }
} }
@ -655,12 +667,6 @@ class SmokeTest {
enhancedTrackingProtection { enhancedTrackingProtection {
verifyEnhancedTrackingProtectionNotice() verifyEnhancedTrackingProtectionNotice()
}.closeNotificationPopup {} }.closeNotificationPopup {}
browserScreen {
}.openThreeDotMenu {
}.openReportSiteIssue {
verifyUrl("webcompat.com/issues/new")
}
} }
@Test @Test
@ -900,10 +906,13 @@ class SmokeTest {
mDevice.waitForIdle() mDevice.waitForIdle()
}.goToHomescreen { }.goToHomescreen {
}.clickSaveTabsToCollectionButton { }.clickSaveTabsToCollectionButton {
selectTab(firstWebPage.title) longClickTab(firstWebPage.title)
selectTab(secondWebPage.title) selectTab(secondWebPage.title)
clickSaveCollection() }.clickSaveCollection {
typeCollectionName(collectionName) typeCollectionNameAndSave(collectionName)
}
tabDrawer {
verifySnackBarText("Collection saved!") verifySnackBarText("Collection saved!")
snackBarButtonClick("VIEW") snackBarButtonClick("VIEW")
} }
@ -914,7 +923,6 @@ class SmokeTest {
} }
} }
@Ignore("Disabling until re-implemented by #19090")
@Test @Test
fun verifyExpandedCollectionItemsTest() { fun verifyExpandedCollectionItemsTest() {
val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -929,14 +937,16 @@ class SmokeTest {
homeScreen { homeScreen {
verifyCollectionIsDisplayed(collectionName) verifyCollectionIsDisplayed(collectionName)
verifyCollectionIcon() verifyCollectionIcon()
expandCollection(collectionName) }.expandCollection(collectionName) {
verifyTabSavedInCollection(webPage.title) verifyTabSavedInCollection(webPage.title)
verifyCollectionTabLogo() verifyCollectionTabLogo()
verifyCollectionTabUrl() verifyCollectionTabUrl()
verifyShareCollectionButtonIsVisible(true) verifyShareCollectionButtonIsVisible(true)
verifyCollectionMenuIsVisible(true) verifyCollectionMenuIsVisible(true)
verifyCollectionItemRemoveButtonIsVisible(webPage.title, true) verifyCollectionItemRemoveButtonIsVisible(webPage.title, true)
collapseCollection(collectionName) }.collapseCollection(collectionName) {}
collectionRobot {
verifyTabSavedInCollection(webPage.title, false) verifyTabSavedInCollection(webPage.title, false)
verifyShareCollectionButtonIsVisible(false) verifyShareCollectionButtonIsVisible(false)
verifyCollectionMenuIsVisible(false) verifyCollectionMenuIsVisible(false)
@ -951,11 +961,12 @@ class SmokeTest {
}.enterURLAndEnterToBrowser(webPage.url) { }.enterURLAndEnterToBrowser(webPage.url) {
}.openTabDrawer { }.openTabDrawer {
createCollection(webPage.title, collectionName) createCollection(webPage.title, collectionName)
verifySnackBarText("Collection saved!")
closeTab() closeTab()
} }
browserScreen {
}.goToHomescreen { homeScreen {
expandCollection(collectionName) }.expandCollection(collectionName) {
clickCollectionThreeDotButton() clickCollectionThreeDotButton()
selectOpenTabs() selectOpenTabs()
} }
@ -964,7 +975,6 @@ class SmokeTest {
} }
} }
@Ignore("Disabling until re-implemented by #19090")
@Test @Test
fun shareCollectionTest() { fun shareCollectionTest() {
val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -975,15 +985,20 @@ class SmokeTest {
createCollection(webPage.title, collectionName) createCollection(webPage.title, collectionName)
snackBarButtonClick("VIEW") snackBarButtonClick("VIEW")
} }
homeScreen { homeScreen {
expandCollection(collectionName) }.expandCollection(collectionName) {
clickShareCollectionButton() clickShareCollectionButton()
}
homeScreen {
verifyShareTabsOverlay() verifyShareTabsOverlay()
} }
} }
@Ignore("Disabling until re-implemented by #19090")
@Test @Test
// Test running on beta/release builds in CI:
// caution when making changes to it, so they don't block the builds
fun deleteCollectionTest() { fun deleteCollectionTest() {
val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -993,11 +1008,15 @@ class SmokeTest {
createCollection(webPage.title, collectionName) createCollection(webPage.title, collectionName)
snackBarButtonClick("VIEW") snackBarButtonClick("VIEW")
} }
homeScreen { homeScreen {
expandCollection(collectionName) }.expandCollection(collectionName) {
clickCollectionThreeDotButton() clickCollectionThreeDotButton()
selectDeleteCollection() selectDeleteCollection()
confirmDeleteCollection() confirmDeleteCollection()
}
homeScreen {
verifyNoCollectionsText() verifyNoCollectionsText()
} }
} }
@ -1098,13 +1117,14 @@ class SmokeTest {
verifyExistingOpenTabs("Test_Page_1") verifyExistingOpenTabs("Test_Page_1")
verifyCloseTabsButton("Test_Page_1") verifyCloseTabsButton("Test_Page_1")
verifyOpenedTabThumbnail() verifyOpenedTabThumbnail()
verifyBrowserTabsTrayURL("localhost")
verifyTabTrayOverflowMenu(true) verifyTabTrayOverflowMenu(true)
verifyNewTabButton() verifyNewTabButton()
} }
} }
@Test @Test
// Test running on beta/release builds in CI:
// caution when making changes to it, so they don't block the builds
fun noHistoryInPrivateBrowsingTest() { fun noHistoryInPrivateBrowsingTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1) val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -1132,11 +1152,16 @@ class SmokeTest {
verifyAddPrivateBrowsingShortcutButton() verifyAddPrivateBrowsingShortcutButton()
clickAddPrivateBrowsingShortcutButton() clickAddPrivateBrowsingShortcutButton()
clickAddAutomaticallyButton() clickAddAutomaticallyButton()
}.openHomeScreenShortcut("Private Firefox Preview") { }.openHomeScreenShortcut("Private $appName") {}
searchScreen {
verifySearchView()
}.dismissSearchBar {
verifyPrivateSessionMessage()
} }
} }
@Test @Test
@Ignore("To be re-enabled later. See https://github.com/mozilla-mobile/fenix/issues/20716")
fun mainMenuInstallPWATest() { fun mainMenuInstallPWATest() {
val pwaPage = "https://rpappalax.github.io/testapp/" 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 @Test
fun restoreTabCrashedReporterTest() { fun restoreTabCrashedReporterTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1) val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -1234,9 +1260,7 @@ class SmokeTest {
) )
customTabScreen { customTabScreen {
browserScreen { verifyCustomTabCloseButton()
verifyPageContent(customTabPage.content)
}
}.openMainMenu { }.openMainMenu {
verifyPoweredByTextIsDisplayed() verifyPoweredByTextIsDisplayed()
verifyCustomMenuItem(customMenuItem) verifyCustomMenuItem(customMenuItem)
@ -1261,9 +1285,7 @@ class SmokeTest {
) )
customTabScreen { customTabScreen {
browserScreen { verifyCustomTabCloseButton()
verifyPageContent(customTabPage.content)
}
}.openMainMenu { }.openMainMenu {
}.clickOpenInBrowserButton { }.clickOpenInBrowserButton {
verifyTabCounter("1") verifyTabCounter("1")
@ -1320,4 +1342,174 @@ class SmokeTest {
assertPlaybackState(browserStore, MediaSession.PlaybackState.PAUSED) 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.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.homeScreen
@ -49,11 +48,7 @@ class ThreeDotMenuMainTest {
verifyHistoryButton() verifyHistoryButton()
verifyDownloadsButton() verifyDownloadsButton()
verifyAddOnsButton() verifyAddOnsButton()
if (FeatureFlags.tabsTrayRewrite) { verifySyncSignInButton()
verifySyncSignInButton()
} else {
verifySyncedTabsButton()
}
verifyDesktopSite() verifyDesktopSite()
verifyWhatsNewButton() verifyWhatsNewButton()
verifyHelpButton() 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 disconnectButton() = Espresso.onView(CoreMatchers.allOf(ViewMatchers.withId(R.id.signOutDisconnect)))
private fun assertBookmarksCheckbox() = bookmarksCheckbox().check( private fun assertBookmarksCheckbox() = bookmarksCheckbox().check(
ViewAssertions.matches( ViewAssertions.matches(
ViewMatchers.withEffectiveVisibility( ViewMatchers.withEffectiveVisibility(
ViewMatchers.Visibility.VISIBLE ViewMatchers.Visibility.VISIBLE
)
) )
)
) )
private fun assertHistoryCheckbox() = historyCheckbox().check( private fun assertHistoryCheckbox() = historyCheckbox().check(
ViewAssertions.matches( ViewAssertions.matches(
ViewMatchers.withEffectiveVisibility( ViewMatchers.withEffectiveVisibility(
ViewMatchers.Visibility.VISIBLE ViewMatchers.Visibility.VISIBLE
)
) )
)
) )
private fun assertSignOutButton() = signOutButton().check(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 shortcutNameField() = onView(withId(R.id.shortcut_text))
private fun assertShortcutNameField(expectedText: String) { private fun assertShortcutNameField(expectedText: String) {
onView(allOf(withId(R.id.shortcut_text), onView(
withText(expectedText))) allOf(
withId(R.id.shortcut_text),
withText(expectedText)
)
)
.check(matches(isCompletelyDisplayed())) .check(matches(isCompletelyDisplayed()))
} }

@ -105,8 +105,10 @@ class BookmarksRobot {
fun verifySelectDefaultFolderSnackBarText() = assertSnackBarText("Cant edit default folders") fun verifySelectDefaultFolderSnackBarText() = assertSnackBarText("Cant edit default folders")
fun verifyCurrentFolderTitle(title: String) { fun verifyCurrentFolderTitle(title: String) {
mDevice.findObject(UiSelector().resourceId("$packageName:id/navigationToolbar") mDevice.findObject(
.textContains(title)) UiSelector().resourceId("$packageName:id/navigationToolbar")
.textContains(title)
)
.waitForExists(waitingTime) .waitForExists(waitingTime)
onView( onView(
@ -119,8 +121,10 @@ class BookmarksRobot {
} }
fun waitForBookmarksFolderContentToExist(parentFolderName: String, childFolderName: String) { fun waitForBookmarksFolderContentToExist(parentFolderName: String, childFolderName: String) {
mDevice.findObject(UiSelector().resourceId("$packageName:id/navigationToolbar") mDevice.findObject(
.textContains(parentFolderName)) UiSelector().resourceId("$packageName:id/navigationToolbar")
.textContains(parentFolderName)
)
.waitForExists(waitingTime) .waitForExists(waitingTime)
mDevice.waitNotNull(Until.findObject(By.text(childFolderName)), 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 * 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, You can obtain one at http://mozilla.org/MPL/2.0/. */
@file:Suppress("TooManyFunctions") @file:Suppress("TooManyFunctions", "TooGenericExceptionCaught")
package org.mozilla.fenix.ui.robots 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.Visibility
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed 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.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withResourceName import androidx.test.espresso.matcher.ViewMatchers.withResourceName
@ -333,6 +334,59 @@ class BrowserRobot {
element.click(LONG_CLICK_DURATION) 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) { fun snackBarButtonClick(expectedText: String) {
onView(allOf(withId(R.id.snackbar_btn), withText(expectedText))).check( onView(allOf(withId(R.id.snackbar_btn), withText(expectedText))).check(
matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)) matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))
@ -455,8 +509,10 @@ class BrowserRobot {
fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition { fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
mDevice.waitForIdle(waitingTime) mDevice.waitForIdle(waitingTime)
tabsCounter().click() tabsCounter().click()
mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/tab_layout")), mDevice.waitNotNull(
waitingTime) Until.findObject(By.res("$packageName:id/tab_layout")),
waitingTime
)
TabDrawerRobot().interact() TabDrawerRobot().interact()
return TabDrawerRobot.Transition() return TabDrawerRobot.Transition()
@ -481,9 +537,9 @@ class BrowserRobot {
} }
fun goToHomescreen(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition { fun goToHomescreen(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
openTabDrawer { onView(withContentDescription("Home screen"))
}.openNewTab { .check(matches(isDisplayed()))
}.dismissSearchBar {} .click()
HomeScreenRobot().interact() HomeScreenRobot().interact()
return HomeScreenRobot.Transition() 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.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed 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.withId
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.UiSelector
import junit.framework.TestCase.assertTrue import junit.framework.TestCase.assertTrue
import mozilla.components.support.ktx.android.content.appName
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.appName
/** /**
* Implementation of the robot pattern for Custom tabs * Implementation of the robot pattern for Custom tabs
@ -29,7 +30,10 @@ class CustomTabRobot {
} }
fun verifyPoweredByTextIsDisplayed() { fun verifyPoweredByTextIsDisplayed() {
mDevice.findObject(UiSelector().textContains("POWERED BY ${appContext.appName}")) assertTrue(
mDevice.findObject(UiSelector().textContains("POWERED BY $appName"))
.waitForExists(waitingTime)
)
} }
fun verifyOpenInBrowserButtonExists() { fun verifyOpenInBrowserButtonExists() {
@ -43,7 +47,11 @@ class CustomTabRobot {
fun verifyRefreshButtonExists() = assertTrue(refreshButton().waitForExists(waitingTime)) fun verifyRefreshButtonExists() = assertTrue(refreshButton().waitForExists(waitingTime))
fun verifyCustomMenuItem(label: String) { 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 { 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 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 refreshButton() = mDevice.findObject(UiSelector().description("Refresh"))
private fun forwardButton() = mDevice.findObject(UiSelector().description("Forward")) private fun forwardButton() = mDevice.findObject(UiSelector().description("Forward"))
private fun backButton() = mDevice.findObject(UiSelector().description("Back")) 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.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.matcher.RootMatchers.isDialog 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.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription 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.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
@ -24,13 +24,13 @@ import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers import org.hamcrest.CoreMatchers
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.mozilla.fenix.R 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
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull 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. * Implementation of Robot Pattern for download UI handling.
@ -52,14 +52,16 @@ class DownloadRobot {
fun verifyDownloadedFileIcon() = assertDownloadedFileIcon() fun verifyDownloadedFileIcon() = assertDownloadedFileIcon()
fun verifyEmptyDownloadsList() { 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) .waitForExists(waitingTime)
onView(withText("No downloaded files")).check(matches(isDisplayed())) onView(withText("No downloaded files")).check(matches(isDisplayed()))
} }
fun waitForDownloadsListToExist() = fun waitForDownloadsListToExist() =
assertTrue(mDevice.findObject(UiSelector().resourceId("org.mozilla.fenix.debug:id/download_list")) assertTrue(
.waitForExists(waitingTime)) mDevice.findObject(UiSelector().resourceId("$packageName:id/download_list"))
.waitForExists(waitingTime)
)
class Transition { class Transition {
fun clickDownload(interact: DownloadRobot.() -> Unit): Transition { fun clickDownload(interact: DownloadRobot.() -> Unit): Transition {

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

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

@ -13,15 +13,11 @@ import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click 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.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem
import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.Visibility import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant 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.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withHint import androidx.test.espresso.matcher.ViewMatchers.withHint
@ -38,7 +34,6 @@ import androidx.test.uiautomator.Until
import androidx.test.uiautomator.Until.findObject import androidx.test.uiautomator.Until.findObject
import junit.framework.TestCase.assertTrue import junit.framework.TestCase.assertTrue
import mozilla.components.browser.state.state.searchEngines import mozilla.components.browser.state.state.searchEngines
import mozilla.components.support.ktx.android.content.appName
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.instanceOf import org.hamcrest.CoreMatchers.instanceOf
@ -48,6 +43,8 @@ import org.junit.Assert
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime 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.packageName
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
@ -63,10 +60,10 @@ import org.mozilla.fenix.ui.util.STRING_ONBOARDING_TRACKING_PROTECTION_HEADER
*/ */
class HomeScreenRobot { class HomeScreenRobot {
val privateSessionMessage = val privateSessionMessage =
"${appContext.appName} clears your search and browsing history from private tabs when you close them" + "$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" + " 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" + " service provider, it makes it easier to keep what you do online private from anyone" +
" else who uses this device." " else who uses this device."
fun verifyNavigationToolbar() = assertNavigationToolbar() fun verifyNavigationToolbar() = assertNavigationToolbar()
fun verifyFocusedNavigationToolbar() = assertFocusedNavigationToolbar() fun verifyFocusedNavigationToolbar() = assertFocusedNavigationToolbar()
@ -119,127 +116,21 @@ class HomeScreenRobot {
fun verifyExistingTopSitesTabs(title: String) = assertExistingTopSitesTabs(title) fun verifyExistingTopSitesTabs(title: String) = assertExistingTopSitesTabs(title)
fun verifyTopSiteContextMenuItems() = assertTopSiteContextMenuItems() fun verifyTopSiteContextMenuItems() = assertTopSiteContextMenuItems()
// Collections element // Collections elements
fun clickCollectionThreeDotButton() { fun verifyCollectionIsDisplayed(title: String, collectionExists: Boolean = true) {
collectionThreeDotButton().click() if (collectionExists) {
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) {
scrollToElementByText(title) 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) { fun verifyCollectionIcon() = onView(withId(R.id.collection_icon)).check(matches(isDisplayed()))
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 verifyShareTabsOverlay() = assertShareTabsOverlay() 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() { fun togglePrivateBrowsingModeOnOff() {
onView(ViewMatchers.withResourceName("privateBrowsingButton")) onView(ViewMatchers.withResourceName("privateBrowsingButton"))
.perform(click()) .perform(click())
@ -316,7 +207,7 @@ class HomeScreenRobot {
} }
fun triggerPrivateBrowsingShortcutPrompt(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenRobot.Transition { 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) { for (i in 1..5) {
mDevice.findObject(UiSelector().resourceId("$packageName:id/privateBrowsingButton")) mDevice.findObject(UiSelector().resourceId("$packageName:id/privateBrowsingButton"))
.waitForExists( .waitForExists(
@ -415,6 +306,19 @@ class HomeScreenRobot {
TabDrawerRobot().interact() TabDrawerRobot().interact()
return TabDrawerRobot.Transition() 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 mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
private fun homeScreenList() = private fun homeScreenList() =
UiScrollable( UiScrollable(
@ -453,7 +356,7 @@ private fun assertFocusedNavigationToolbar() =
private fun assertHomeScreen() { private fun assertHomeScreen() {
mDevice.findObject(UiSelector().resourceId("$packageName:id/homeLayout")).waitForExists(waitingTime) mDevice.findObject(UiSelector().resourceId("$packageName:id/homeLayout")).waitForExists(waitingTime)
onView(ViewMatchers.withResourceName("homeLayout")) onView(ViewMatchers.withResourceName("homeLayout"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
} }
private fun assertHomeMenu() = onView(ViewMatchers.withResourceName("menuButton")) private fun assertHomeMenu() = onView(ViewMatchers.withResourceName("menuButton"))
@ -480,7 +383,8 @@ private fun assertCollectionsHeader() =
private fun assertNoCollectionsText() = private fun assertNoCollectionsText() =
onView( onView(
withText( 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." "Group together similar searches, sites, and tabs for quick access later."
) )
) )
@ -510,7 +414,7 @@ private fun verifySearchEngineIcon(searchEngineName: String) {
// First Run elements // First Run elements
private fun assertWelcomeHeader() = private fun assertWelcomeHeader() =
onView(allOf(withText("Welcome to ${appContext.appName}!"))) onView(allOf(withText("Welcome to $appName!")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertStartSyncHeader() { private fun assertStartSyncHeader() {
@ -597,7 +501,7 @@ private fun assertYourPrivacyText() {
onView( onView(
allOf( allOf(
withText( 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)) onView(withId(R.id.private_session_description))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun collectionThreeDotButton() =
onView(allOf(withId(R.id.collection_overflow_button)))
private fun collectionTitle(title: String) = private fun collectionTitle(title: String) =
onView(allOf(withId(R.id.collection_title), withText(title))) 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 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 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 tabsCounter() = onView(withId(R.id.tab_button))
private fun startBrowsingButton(): UiObject { private fun startBrowsingButton(): UiObject {

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

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

@ -18,6 +18,7 @@ import org.hamcrest.Matchers
import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.allOf
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
/** /**
@ -27,7 +28,7 @@ import org.mozilla.fenix.helpers.click
class RecentlyClosedTabsRobot { class RecentlyClosedTabsRobot {
fun waitForListToExist() = fun waitForListToExist() =
mDevice.findObject(UiSelector().resourceId("org.mozilla.fenix.debug:id/recently_closed_list")) mDevice.findObject(UiSelector().resourceId("$packageName:id/recently_closed_list"))
.waitForExists( .waitForExists(
TestAssetHelper.waitingTime TestAssetHelper.waitingTime
) )
@ -93,7 +94,8 @@ private fun assertRecentlyClosedTabsMenuView() {
) )
) )
.check( .check(
matches(withEffectiveVisibility(Visibility.VISIBLE))) matches(withEffectiveVisibility(Visibility.VISIBLE))
)
} }
private fun assertEmptyRecentlyClosedTabsList() = private fun assertEmptyRecentlyClosedTabsList() =
@ -104,7 +106,8 @@ private fun assertEmptyRecentlyClosedTabsList() =
) )
) )
.check( .check(
matches(withText("No recently closed tabs here"))) matches(withText("No recently closed tabs here"))
)
private fun assertPageUrl(expectedUrl: Uri) = onView( private fun assertPageUrl(expectedUrl: Uri) = onView(
allOf( allOf(
@ -115,7 +118,8 @@ private fun assertPageUrl(expectedUrl: Uri) = onView(
) )
) )
.check( .check(
matches(withText(Matchers.containsString(expectedUrl.toString())))) matches(withText(Matchers.containsString(expectedUrl.toString())))
)
private fun recentlyClosedTabsPageTitle() = onView( private fun recentlyClosedTabsPageTitle() = onView(
allOf( allOf(
@ -127,9 +131,11 @@ private fun recentlyClosedTabsPageTitle() = onView(
private fun assertRecentlyClosedTabsPageTitle(title: String) { private fun assertRecentlyClosedTabsPageTitle(title: String) {
recentlyClosedTabsPageTitle() recentlyClosedTabsPageTitle()
.check( .check(
matches(withEffectiveVisibility(Visibility.VISIBLE))) matches(withEffectiveVisibility(Visibility.VISIBLE))
)
.check( .check(
matches(withText(title))) matches(withText(title))
)
} }
private fun recentlyClosedTabsThreeDotButton() = private fun recentlyClosedTabsThreeDotButton() =
@ -137,45 +143,50 @@ private fun recentlyClosedTabsThreeDotButton() =
allOf( allOf(
withId(R.id.overflow_menu), withId(R.id.overflow_menu),
withEffectiveVisibility( withEffectiveVisibility(
Visibility.VISIBLE Visibility.VISIBLE
)
) )
) )
)
private fun assertRecentlyClosedTabsMenuCopy() = private fun assertRecentlyClosedTabsMenuCopy() =
onView(withText("Copy")) onView(withText("Copy"))
.check( .check(
matches( matches(
withEffectiveVisibility(Visibility.VISIBLE))) withEffectiveVisibility(Visibility.VISIBLE)
)
)
private fun assertRecentlyClosedTabsMenuShare() = private fun assertRecentlyClosedTabsMenuShare() =
onView(withText("Share")) onView(withText("Share"))
.check( .check(
matches( matches(
withEffectiveVisibility(Visibility.VISIBLE))) withEffectiveVisibility(Visibility.VISIBLE)
)
)
private fun assertRecentlyClosedTabsOverlayNewTab() = private fun assertRecentlyClosedTabsOverlayNewTab() =
onView(withText("Open in new tab")) onView(withText("Open in new tab"))
.check( .check(
matches( matches(
withEffectiveVisibility(Visibility.VISIBLE)) withEffectiveVisibility(Visibility.VISIBLE)
) )
)
private fun assertRecentlyClosedTabsMenuPrivateTab() = private fun assertRecentlyClosedTabsMenuPrivateTab() =
onView(withText("Open in private tab")) onView(withText("Open in private tab"))
.check( .check(
matches( matches(
withEffectiveVisibility(Visibility.VISIBLE) withEffectiveVisibility(Visibility.VISIBLE)
)
) )
)
private fun assertRecentlyClosedTabsMenuDelete() = private fun assertRecentlyClosedTabsMenuDelete() =
onView(withText("Delete")) onView(withText("Delete"))
.check( .check(
matches( matches(
withEffectiveVisibility(Visibility.VISIBLE) withEffectiveVisibility(Visibility.VISIBLE)
) )
) )
private fun recentlyClosedTabsCopyButton() = onView(withText("Copy")) private fun recentlyClosedTabsCopyButton() = onView(withText("Copy"))
@ -184,26 +195,31 @@ private fun copySnackBarText() = onView(withId(R.id.snackbar_text))
private fun assertCopySnackBarText() = copySnackBarText() private fun assertCopySnackBarText() = copySnackBarText()
.check( .check(
matches matches
(withText("URL copied"))) (withText("URL copied"))
)
private fun recentlyClosedTabsShareButton() = onView(withText("Share")) private fun recentlyClosedTabsShareButton() = onView(withText("Share"))
private fun assertRecentlyClosedShareOverlay() = private fun assertRecentlyClosedShareOverlay() =
onView(withId(R.id.shareWrapper)) onView(withId(R.id.shareWrapper))
.check( .check(
matches(ViewMatchers.isDisplayed())) matches(ViewMatchers.isDisplayed())
)
private fun assetRecentlyClosedShareTitle(title: String) = private fun assetRecentlyClosedShareTitle(title: String) =
onView(withId(R.id.share_tab_title)) onView(withId(R.id.share_tab_title))
.check( .check(
matches(ViewMatchers.isDisplayed())) matches(ViewMatchers.isDisplayed())
)
.check( .check(
matches(withText(title))) matches(withText(title))
)
private fun assertRecentlyClosedShareFavicon() = private fun assertRecentlyClosedShareFavicon() =
onView(withId(R.id.share_tab_favicon)) onView(withId(R.id.share_tab_favicon))
.check( .check(
matches(ViewMatchers.isDisplayed())) matches(ViewMatchers.isDisplayed())
)
private fun assertRecentlyClosedShareUrl(expectedUrl: Uri) = private fun assertRecentlyClosedShareUrl(expectedUrl: Uri) =
onView( onView(
@ -213,7 +229,8 @@ private fun assertRecentlyClosedShareUrl(expectedUrl: Uri) =
) )
) )
.check( .check(
matches(withText(Matchers.containsString(expectedUrl.toString())))) matches(withText(Matchers.containsString(expectedUrl.toString())))
)
private fun recentlyClosedTabsNewTabButton() = onView(withText("Open in new tab")) 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.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withSubstring
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By import androidx.test.uiautomator.By
@ -35,6 +36,7 @@ import org.hamcrest.CoreMatchers.startsWith
import org.hamcrest.Matchers import org.hamcrest.Matchers
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.mozilla.fenix.R 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.SessionLoadedIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
@ -71,8 +73,10 @@ class SearchRobot {
selectDefaultSearchEngine(searchEngineName) selectDefaultSearchEngine(searchEngineName)
fun clickSearchEngineShortcutButton() { fun clickSearchEngineShortcutButton() {
val searchEnginesShortcutButton = mDevice.findObject(UiSelector() val searchEnginesShortcutButton = mDevice.findObject(
.resourceId("$packageName:id/search_engines_shortcut_button")) UiSelector()
.resourceId("$packageName:id/search_engines_shortcut_button")
)
searchEnginesShortcutButton.waitForExists(waitingTime) searchEnginesShortcutButton.waitForExists(waitingTime)
searchEnginesShortcutButton.click() searchEnginesShortcutButton.click()
} }
@ -126,6 +130,24 @@ class SearchRobot {
clearButton().perform(click()) 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 { class Transition {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource
@ -202,10 +224,10 @@ private fun searchWrapper() = onView(withId(R.id.search_wrapper))
private fun assertSearchEngineURL(searchEngineName: String) { private fun assertSearchEngineURL(searchEngineName: String) {
mDevice.waitNotNull( mDevice.waitNotNull(
Until.findObject(By.textContains("${searchEngineName.toLowerCase()}.com/?q=mozilla")), Until.findObject(By.textContains("${searchEngineName.lowercase()}.com/?q=mozilla")),
TestAssetHelper.waitingTime TestAssetHelper.waitingTime
) )
onView(allOf(withText(startsWith("${searchEngineName.toLowerCase()}.com")))) onView(allOf(withText(startsWith("${searchEngineName.lowercase()}.com"))))
.check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) .check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
} }
@ -250,7 +272,8 @@ private fun assertKeyboardVisibility(isExpectedToBeVisible: Boolean) = {
mDevice.waitNotNull( mDevice.waitNotNull(
Until.findObject( Until.findObject(
By.text("Search Engine") By.text("Search Engine")
), waitingTime ),
waitingTime
) )
assertEquals( assertEquals(
isExpectedToBeVisible, isExpectedToBeVisible,
@ -295,4 +318,17 @@ private fun assertDefaultSearchEngine(expectedText: String) {
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .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"))) 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.R
import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_PLAY_SERVICES import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_PLAY_SERVICES
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime 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.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.assertIsEnabled import org.mozilla.fenix.helpers.assertIsEnabled
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
@ -110,7 +111,7 @@ class SettingsRobot {
} }
fun openAboutFirefoxPreview(interact: SettingsSubMenuAboutRobot.() -> Unit): fun openAboutFirefoxPreview(interact: SettingsSubMenuAboutRobot.() -> Unit):
SettingsSubMenuAboutRobot.Transition { SettingsSubMenuAboutRobot.Transition {
assertAboutFirefoxPreview().click() assertAboutFirefoxPreview().click()
@ -119,7 +120,7 @@ class SettingsRobot {
} }
fun openSearchSubMenu(interact: SettingsSubMenuSearchRobot.() -> Unit): fun openSearchSubMenu(interact: SettingsSubMenuSearchRobot.() -> Unit):
SettingsSubMenuSearchRobot.Transition { SettingsSubMenuSearchRobot.Transition {
fun searchEngineButton() = onView(withText("Search")) fun searchEngineButton() = onView(withText("Search"))
searchEngineButton().click() searchEngineButton().click()
@ -147,15 +148,19 @@ class SettingsRobot {
} }
fun openAccessibilitySubMenu(interact: SettingsSubMenuAccessibilityRobot.() -> Unit): SettingsSubMenuAccessibilityRobot.Transition { fun openAccessibilitySubMenu(interact: SettingsSubMenuAccessibilityRobot.() -> Unit): SettingsSubMenuAccessibilityRobot.Transition {
scrollToElementByText("Accessibility")
fun accessibilityButton() = onView(withText("Accessibility")) fun accessibilityButton() = onView(withText("Accessibility"))
accessibilityButton().click() accessibilityButton()
.check(matches(isDisplayed()))
.click()
SettingsSubMenuAccessibilityRobot().interact() SettingsSubMenuAccessibilityRobot().interact()
return SettingsSubMenuAccessibilityRobot.Transition() return SettingsSubMenuAccessibilityRobot.Transition()
} }
fun openLanguageSubMenu(interact: SettingsSubMenuLanguageRobot.() -> Unit): SettingsSubMenuLanguageRobot.Transition { fun openLanguageSubMenu(interact: SettingsSubMenuLanguageRobot.() -> Unit): SettingsSubMenuLanguageRobot.Transition {
scrollToElementByText("Language")
fun languageButton() = onView(withText("Language")) fun languageButton() = onView(withText("Language"))
languageButton().click() languageButton().click()
@ -310,14 +315,8 @@ private fun assertDefaultBrowserIsDisabled() {
} }
private fun toggleDefaultBrowserSwitch() { private fun toggleDefaultBrowserSwitch() {
scrollToElementByText("Set as default browser") scrollToElementByText("Privacy and security")
onView( onView(withText("Set as default browser")).perform(ViewActions.click())
CoreMatchers.allOf(
ViewMatchers.withParent(CoreMatchers.not(withId(R.id.navigationToolbar))),
withText("Set as default browser")
)
)
.perform(ViewActions.click())
} }
private fun assertAndroidDefaultAppsMenuAppears() { private fun assertAndroidDefaultAppsMenuAppears() {
@ -402,7 +401,7 @@ private fun assertNotificationsButton() {
private fun assertDataCollectionButton() { private fun assertDataCollectionButton() {
scrollToElementByText("Data collection") scrollToElementByText("Data collection")
onView(withText("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")) private fun openLinksInAppsButton() = onView(withText("Open links in apps"))
@ -475,8 +474,8 @@ private fun assertRateOnGooglePlay(): ViewInteraction {
private fun assertAboutFirefoxPreview(): ViewInteraction { private fun assertAboutFirefoxPreview(): ViewInteraction {
onView(withId(R.id.recycler_view)) onView(withId(R.id.recycler_view))
.perform(RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(hasDescendant(withText("About Firefox Preview")))) .perform(RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(hasDescendant(withText("About $appName"))))
return onView(withText("About Firefox Preview")) return onView(withText("About $appName"))
.check(matches(isDisplayed())) .check(matches(isDisplayed()))
} }

@ -28,6 +28,7 @@ import org.hamcrest.CoreMatchers.containsString
import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestHelper import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.isVisibleForUser import org.mozilla.fenix.helpers.isVisibleForUser
import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.settings.SupportUtils
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -108,7 +109,7 @@ private fun assertVersionNumber() {
private fun assertProductCompany() { private fun assertProductCompany() {
onView(withId(R.id.about_content)) 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() { 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' // 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 // instead of the date. See https://github.com/mozilla-mobile/fenix/pull/10812#issuecomment-633746833
.check(matches(withText(containsString("debug build")))) .check(matches(withText(containsString("debug build"))))
// This assertion should be valid for non-debug build types. // This assertion should be valid for non-debug build types.
// .check(BuildDateAssertion.isDisplayedDateAccurate()) // .check(BuildDateAssertion.isDisplayedDateAccurate())
} }
private fun assertWhatIsNewInFirefoxPreview() { 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(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))) .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.perform(click()) .perform(click())
@ -222,7 +223,7 @@ private fun assertLibrariesUsed() {
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.perform(click()) .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() Espresso.pressBack()
} }

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

@ -16,6 +16,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.assertIsEnabled import org.mozilla.fenix.helpers.assertIsEnabled
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
@ -58,8 +59,11 @@ private fun goBackButton() =
onView(withContentDescription("Navigate up")) onView(withContentDescription("Navigate up"))
private fun assertNavigationToolBarHeader() = onView( private fun assertNavigationToolBarHeader() = onView(
allOf(withParent(withId(R.id.navigationToolbar)), allOf(
withText(R.string.preferences_data_collection))) withParent(withId(R.id.navigationToolbar)),
withText(R.string.preferences_data_collection)
)
)
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertDataCollectionOptions() { private fun assertDataCollectionOptions() {
@ -68,7 +72,7 @@ private fun assertDataCollectionOptions() {
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
val usageAndTechnicalDataText = 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)) onView(withText(usageAndTechnicalDataText))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))

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

@ -21,9 +21,10 @@ import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.mozilla.fenix.R 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.assertIsChecked
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.TestAssetHelper
/** /**
* Implementation of Robot Pattern for the settings Delete Browsing Data sub menu. * Implementation of Robot Pattern for the settings Delete Browsing Data sub menu.
@ -85,8 +86,12 @@ private fun goBackButton() =
onView(allOf(withContentDescription("Navigate up"))) onView(allOf(withContentDescription("Navigate up")))
private fun assertNavigationToolBarHeader() { private fun assertNavigationToolBarHeader() {
onView(allOf(withId(R.id.navigationToolbar), onView(
withChild(withText(R.string.preferences_delete_browsing_data)))) allOf(
withId(R.id.navigationToolbar),
withChild(withText(R.string.preferences_delete_browsing_data))
)
)
.check((matches(withEffectiveVisibility(Visibility.VISIBLE)))) .check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
} }
@ -104,7 +109,7 @@ private fun cancelButton() =
mDevice.findObject(UiSelector().textStartsWith("CANCEL")) mDevice.findObject(UiSelector().textStartsWith("CANCEL"))
private fun assertMessageInDialogBox() = private fun assertMessageInDialogBox() =
onView(withText("Firefox Preview will delete the selected browsing data.")) onView(withText("$appName will delete the selected browsing data."))
.inRoot(isDialog()) .inRoot(isDialog())
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .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.Espresso.pressBack
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions 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.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.hasSibling 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.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withChild import androidx.test.espresso.matcher.ViewMatchers.withChild
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
@ -26,6 +26,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not import org.hamcrest.CoreMatchers.not
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.assertIsChecked import org.mozilla.fenix.helpers.assertIsChecked
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
@ -110,8 +111,12 @@ class SettingsSubMenuEnhancedTrackingProtectionRobot {
} }
private fun assertNavigationToolBarHeader() { private fun assertNavigationToolBarHeader() {
onView(allOf(withParent(withId(org.mozilla.fenix.R.id.navigationToolbar)), onView(
withText("Enhanced Tracking Protection"))) allOf(
withParent(withId(org.mozilla.fenix.R.id.navigationToolbar)),
withText("Enhanced Tracking Protection")
)
)
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
} }
@ -121,21 +126,32 @@ private fun assertEnhancedTrackingProtectionHeader() {
} }
private fun assertEnhancedTrackingProtectionHeaderDescription() { private fun assertEnhancedTrackingProtectionHeaderDescription() {
onView(allOf(withParent(withParentIndex(0)), onView(
withText("Keep your data to yourself. Firefox Preview protects you from many of the most common trackers that follow what you do online."))) 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))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
} }
private fun assertLearnMoreText() { private fun assertLearnMoreText() {
onView(allOf(withParent(withParentIndex(0)), onView(
withText("Learn more"))) allOf(
withParent(withParentIndex(0)),
withText("Learn more")
)
)
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
} }
private fun assertEnhancedTrackingProtectionTextWithSwitchWidget() { private fun assertEnhancedTrackingProtectionTextWithSwitchWidget() {
onView(allOf( onView(
allOf(
withParentIndex(1), withParentIndex(1),
withChild(withText("Enhanced Tracking Protection")))) withChild(withText("Enhanced Tracking Protection"))
)
)
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
} }
@ -194,7 +210,8 @@ private fun assertTrackingProtectionSwitchEnabled() {
} }
private fun assertRadioButtonDefaults() { private fun assertRadioButtonDefaults() {
onView(withText("Strict") onView(
withText("Strict")
).assertIsChecked(false) ).assertIsChecked(false)
onView( onView(
@ -204,7 +221,8 @@ private fun assertRadioButtonDefaults() {
) )
).assertIsChecked(true) ).assertIsChecked(true)
onView(withText("Custom") onView(
withText("Custom")
).assertIsChecked(false) ).assertIsChecked(false)
} }

@ -9,9 +9,24 @@ import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice 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.hamcrest.CoreMatchers
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.packageName
class SettingsSubMenuLanguageRobot { 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 { class Transition {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@ -27,3 +42,10 @@ class SettingsSubMenuLanguageRobot {
private fun goBackButton() = private fun goBackButton() =
onView(CoreMatchers.allOf(ViewMatchers.withContentDescription("Navigate up"))) 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 { class SettingsSubMenuLoginsAndPasswordOptionsToSaveRobot {
fun verifySaveLoginsOptionsView() { fun verifySaveLoginsOptionsView() {
onView(ViewMatchers.withText("Ask to save")) onView(ViewMatchers.withText("Ask to save"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
onView(ViewMatchers.withText("Never save")) onView(ViewMatchers.withText("Never save"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
} }
class Transition { class Transition {
@ -35,4 +35,4 @@ class SettingsSubMenuLoginsAndPasswordOptionsToSaveRobot {
} }
private fun goBackButton() = 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 package org.mozilla.fenix.ui.robots
import android.content.Context
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions.matches 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.UiDevice
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers import org.hamcrest.CoreMatchers
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.ext.waitNotNull
@ -40,7 +42,7 @@ class SettingsSubMenuLoginsAndPasswordRobot {
fun verifyDefaultValueExceptions() = assertDefaultValueExceptions() fun verifyDefaultValueExceptions() = assertDefaultValueExceptions()
fun verifyDefaultValueAutofillLogins() = assertDefaultValueAutofillLogins() fun verifyDefaultValueAutofillLogins(context: Context) = assertDefaultValueAutofillLogins(context)
class Transition { class Transition {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@ -92,16 +94,23 @@ fun settingsSubMenuLoginsAndPassword(interact: SettingsSubMenuLoginsAndPasswordR
} }
private fun goBackButton() = 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")) 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))) .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun assertDefaultValueExceptions() = onView(ViewMatchers.withText("Exceptions")) private fun assertDefaultValueExceptions() = onView(ViewMatchers.withText("Exceptions"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun assertDefaultValueSyncLogins() = onView(ViewMatchers.withText("Sign in to Sync")) 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.junit.Assert.assertTrue
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime 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.click
import org.mozilla.fenix.helpers.isEnabled import org.mozilla.fenix.helpers.isEnabled
@ -104,7 +105,7 @@ private fun goBackButton() = onView(withContentDescription("Navigate up"))
private fun addAutomaticallyButton() = private fun addAutomaticallyButton() =
mDevice.findObject(UiSelector().textStartsWith("add automatically")) 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() { private fun assertAddPrivateBrowsingShortcutButton() {
mDevice.wait( mDevice.wait(
@ -130,6 +131,6 @@ private fun assertOpenLinksInPrivateTabOff() {
} }
private fun assertPrivateBrowsingShortcutIcon() { private fun assertPrivateBrowsingShortcutIcon() {
mDevice.wait(Until.findObject(text("Private Firefox Preview")), waitingTime) mDevice.wait(Until.findObject(text("Private $appName")), waitingTime)
assertTrue(mDevice.hasObject(text("Private Firefox Preview"))) assertTrue(mDevice.hasObject(text("Private $appName")))
} }

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

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

@ -159,7 +159,7 @@ class SettingsSubMenuSitePermissionsRobot {
onView(withText("Autoplay")) onView(withText("Autoplay"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
val autoplayText = "Block audio and video" val autoplayText = "Block audio only"
onView(withText(autoplayText)) onView(withText(autoplayText))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .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.Espresso.onView
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions 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
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.mozilla.fenix.helpers.click
/** /**
* Implementation of Robot Pattern for the settings Tabs sub menu. * Implementation of Robot Pattern for the settings Tabs sub menu.
@ -22,6 +25,10 @@ class SettingsSubMenuTabsRobot {
fun verifyOptions() = assertOptions() fun verifyOptions() = assertOptions()
fun verifyStartOnHomeOptions() = assertStartOnHomeOptions()
fun clickAlwaysStartOnHomeToggle() = alwaysStartOnHomeToggle().click()
class Transition { class Transition {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@ -46,6 +53,17 @@ private fun assertOptions() {
.check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) .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 manualToggle() = onView(withText("Manually"))
private fun afterOneDayToggle() = onView(withText("After one day")) 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 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() = private fun goBackButton() =
onView(allOf(ViewMatchers.withContentDescription("Navigate up"))) onView(allOf(ViewMatchers.withContentDescription("Navigate up")))

@ -36,6 +36,10 @@ class SettingsSubMenuThemeRobot {
fun selectLightMode() = lightModeToggle().click() fun selectLightMode() = lightModeToggle().click()
fun clickTopToolbarToggle() = topToolbarToggle().click()
fun clickBottomToolbarToggle() = bottomToolbarToggle().click()
class Transition { class Transition {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@ -62,6 +66,10 @@ private fun darkModeToggle() = onView(withText("Dark"))
private fun lightModeToggle() = onView(withText("Light")) private fun lightModeToggle() = onView(withText("Light"))
private fun topToolbarToggle() = onView(withText("Top"))
private fun bottomToolbarToggle() = onView(withText("Bottom"))
private fun deviceModeToggle(): ViewInteraction { private fun deviceModeToggle(): ViewInteraction {
val followDeviceThemeText = val followDeviceThemeText =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) "Follow device theme" else "Set by Battery Saver" 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() = 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")) 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")) 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")) 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() { private fun assertSystemNotificationsView() {
mDevice.findObject(UiSelector().resourceId("com.android.settings:id/list")) mDevice.findObject(UiSelector().resourceId("com.android.settings:id/list"))
.waitForExists(waitingTime) .waitForExists(waitingTime)
assertTrue(mDevice.findObject(UiSelector().textContains("Show notifications")) assertTrue(
.waitForExists(waitingTime) 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.GeneralLocation
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click 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.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions 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
import androidx.test.uiautomator.Until.findObject import androidx.test.uiautomator.Until.findObject
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import junit.framework.TestCase.assertTrue
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.anyOf import org.hamcrest.CoreMatchers.anyOf
import org.hamcrest.CoreMatchers.containsString import org.hamcrest.CoreMatchers.containsString
@ -60,7 +61,7 @@ class TabDrawerRobot {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mDevice.waitNotNull( 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 waitingTime
) )
onView(withId(R.id.mozac_browser_tabstray_url)) onView(withId(R.id.mozac_browser_tabstray_url))
@ -88,7 +89,7 @@ class TabDrawerRobot {
fun closeTab() { fun closeTab() {
mDevice.findObject( mDevice.findObject(
UiSelector().resourceId("org.mozilla.fenix.debug:id/mozac_browser_tabstray_close") UiSelector().resourceId("$packageName:id/mozac_browser_tabstray_close")
).waitForExists(waitingTime) ).waitForExists(waitingTime)
var retries = 0 // number of retries before failing, will stop at 2 var retries = 0 // number of retries before failing, will stop at 2
@ -96,7 +97,7 @@ class TabDrawerRobot {
closeTabButton().click() closeTabButton().click()
retries++ retries++
} while (mDevice.findObject( } 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 ).exists() && retries < 3
) )
} }
@ -150,7 +151,7 @@ class TabDrawerRobot {
mDevice.waitNotNull( mDevice.waitNotNull(
findObject( findObject(
By By
.res("org.mozilla.fenix.debug:id/play_pause_button") .res("$packageName:id/play_pause_button")
.desc(action) .desc(action)
), ),
waitingTime waitingTime
@ -173,8 +174,6 @@ class TabDrawerRobot {
selectTabsButton.click() selectTabsButton.click()
} }
fun clickAddNewCollection() = addNewCollectionButton().click()
fun selectTab(title: String) { fun selectTab(title: String) {
mDevice.waitNotNull( mDevice.waitNotNull(
findObject(text(title)), findObject(text(title)),
@ -185,11 +184,14 @@ class TabDrawerRobot {
tab.click() tab.click()
} }
fun clickSaveCollection() = saveTabsToCollectionButton().click() fun longClickTab(title: String) {
mDevice.waitNotNull(
findObject(text(title)),
waitingTime
)
fun typeCollectionName(collectionName: String) { val tab = onView(withText(title))
collectionNameTextField().perform(replaceText(collectionName)) tab.perform(longClick())
mDevice.findObject(UiSelector().textContains("OK")).click()
} }
fun createCollection( fun createCollection(
@ -199,10 +201,19 @@ class TabDrawerRobot {
) { ) {
clickSelectTabs() clickSelectTabs()
selectTab(tabTitle) selectTab(tabTitle)
clickSaveCollection() tabDrawer {
if (!firstCollection) }.clickSaveCollection {
clickAddNewCollection() if (!firstCollection)
typeCollectionName(collectionName) clickAddNewCollection()
typeCollectionNameAndSave(collectionName)
}
}
fun verifyTabsMultiSelectionCounter(numOfTabs: Int) {
assertTrue(
mDevice.findObject(UiSelector().text("$numOfTabs selected"))
.waitForExists(waitingTime)
)
} }
class Transition { class Transition {
@ -219,8 +230,10 @@ class TabDrawerRobot {
fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition { fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
mDevice.waitForIdle(waitingTime) mDevice.waitForIdle(waitingTime)
tabsCounter().click() tabsCounter().click()
mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/tab_layout")), mDevice.waitNotNull(
waitingTime) Until.findObject(By.res("$packageName:id/tab_layout")),
waitingTime
)
TabDrawerRobot().interact() TabDrawerRobot().interact()
return TabDrawerRobot.Transition() return TabDrawerRobot.Transition()
@ -327,7 +340,7 @@ class TabDrawerRobot {
} }
fun openRecentlyClosedTabs(interact: RecentlyClosedTabsRobot.() -> Unit): fun openRecentlyClosedTabs(interact: RecentlyClosedTabsRobot.() -> Unit):
RecentlyClosedTabsRobot.Transition { RecentlyClosedTabsRobot.Transition {
threeDotMenu().click() threeDotMenu().click()
@ -343,6 +356,14 @@ class TabDrawerRobot {
RecentlyClosedTabsRobot().interact() RecentlyClosedTabsRobot().interact()
return RecentlyClosedTabsRobot.Transition() 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) { private fun assertExistingOpenTabs(title: String) {
try { try {
mDevice.findObject(UiSelector() mDevice.findObject(
.resourceId("$packageName:id/mozac_browser_tabstray_title") UiSelector()
.textContains(title)) .resourceId("$packageName:id/mozac_browser_tabstray_title")
.textContains(title)
)
.waitForExists(waitingTime) .waitForExists(waitingTime)
tab(title).check(matches(isDisplayed())) tab(title).check(matches(isDisplayed()))
@ -464,8 +487,4 @@ private fun tabsCounter() = onView(withId(R.id.tab_button))
private fun visibleOrGone(visibility: Boolean) = private fun visibleOrGone(visibility: Boolean) =
if (visibility) ViewMatchers.Visibility.VISIBLE else ViewMatchers.Visibility.GONE 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 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( mDevice.waitNotNull(
Until.findObject( Until.findObject(
By.text("ALL ACTIONS") By.text("ALL ACTIONS")
), TestAssetHelper.waitingTime ),
TestAssetHelper.waitingTime
) )
LibrarySubMenusMultipleSelectionToolbarRobot().interact() LibrarySubMenusMultipleSelectionToolbarRobot().interact()

@ -8,7 +8,6 @@ package org.mozilla.fenix.ui.robots
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView 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.swipeDown
import androidx.test.espresso.action.ViewActions.swipeUp import androidx.test.espresso.action.ViewActions.swipeUp
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
@ -33,6 +32,7 @@ import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime 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.click
import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.share.ShareFragment import org.mozilla.fenix.share.ShareFragment
@ -50,7 +50,6 @@ class ThreeDotMenuMainRobot {
fun verifyAddOnsButton() = assertAddOnsButton() fun verifyAddOnsButton() = assertAddOnsButton()
fun verifyHistoryButton() = assertHistoryButton() fun verifyHistoryButton() = assertHistoryButton()
fun verifyBookmarksButton() = assertBookmarksButton() fun verifyBookmarksButton() = assertBookmarksButton()
fun verifySyncedTabsButton() = assertSyncedTabsButton()
fun verifySyncSignInButton() = assertSignInToSyncButton() fun verifySyncSignInButton() = assertSignInToSyncButton()
fun verifyHelpButton() = assertHelpButton() fun verifyHelpButton() = assertHelpButton()
fun verifyThreeDotMenuExists() = threeDotMenuRecyclerViewExists() fun verifyThreeDotMenuExists() = threeDotMenuRecyclerViewExists()
@ -80,15 +79,6 @@ class ThreeDotMenuMainRobot {
fun verifySaveCollection() = assertSaveCollectionButton() fun verifySaveCollection() = assertSaveCollectionButton()
fun verifySelectTabs() = assertSelectTabsButton() fun verifySelectTabs() = assertSelectTabsButton()
fun clickSaveCollectionButton() {
browserViewSaveCollectionButton().click()
}
fun clickAddNewCollection() {
addNewCollectionButton().click()
}
fun verifyCollectionNameTextField() = assertCollectionNameTextField()
fun verifyFindInPageButton() = assertFindInPageButton() fun verifyFindInPageButton() = assertFindInPageButton()
fun verifyShareScrim() = assertShareScrim() fun verifyShareScrim() = assertShareScrim()
fun verifySendToDeviceTitle() = assertSendToDeviceTitle() fun verifySendToDeviceTitle() = assertSendToDeviceTitle()
@ -137,11 +127,11 @@ class ThreeDotMenuMainRobot {
private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
fun openSettings(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition { fun openSettings(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
var maxSwipes = 3 // We require one swipe to display the full size 3-dot menu. On smaller devices
while (!settingsButton().exists() && maxSwipes != 0) { // such as the Pixel 2, we require two swipes to display the "Settings" menu item
threeDotMenuRecyclerView().perform(swipeUp()) // at the bottom. On larger devices, the second swipe is a no-op.
maxSwipes-- threeDotMenuRecyclerView().perform(swipeUp())
} threeDotMenuRecyclerView().perform(swipeUp())
settingsButton().click() settingsButton().click()
SettingsRobot().interact() SettingsRobot().interact()
@ -156,15 +146,6 @@ class ThreeDotMenuMainRobot {
return DownloadRobot.Transition() 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 { fun openSyncSignIn(interact: SyncSignInRobot.() -> Unit): SyncSignInRobot.Transition {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(swipeDown()) onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("Sign in to sync")), waitingTime) 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) mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime)
bookmarksButton().click() 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() BookmarksRobot().interact()
return BookmarksRobot.Transition() return BookmarksRobot.Transition()
@ -296,26 +277,6 @@ class ThreeDotMenuMainRobot {
return BrowserRobot.Transition() 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 { fun openReaderViewAppearance(interact: ReaderViewRobot.() -> Unit): ReaderViewRobot.Transition {
var maxSwipes = 3 var maxSwipes = 3
while (!readerViewAppearanceToggle().exists() && maxSwipes != 0) { while (!readerViewAppearanceToggle().exists() && maxSwipes != 0) {
@ -355,15 +316,7 @@ class ThreeDotMenuMainRobot {
return AddToHomeScreenRobot.Transition() return AddToHomeScreenRobot.Transition()
} }
fun selectExistingCollection(title: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun openSaveToCollection(interact: CollectionRobot.() -> Unit): CollectionRobot.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 {
// Ensure the menu is expanded and fully scrolled to the bottom. // Ensure the menu is expanded and fully scrolled to the bottom.
for (i in 0..3) { for (i in 0..3) {
threeDotMenuRecyclerView().perform(swipeUp()) threeDotMenuRecyclerView().perform(swipeUp())
@ -371,8 +324,8 @@ class ThreeDotMenuMainRobot {
mDevice.waitNotNull(Until.findObject(By.text("Save to collection")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("Save to collection")), waitingTime)
saveCollectionButton().click() saveCollectionButton().click()
ThreeDotMenuMainRobot().interact() CollectionRobot().interact()
return ThreeDotMenuMainRobot.Transition() return CollectionRobot.Transition()
} }
fun openAddonsManagerMenu(interact: SettingsSubMenuAddonsManagerRobot.() -> Unit): SettingsSubMenuAddonsManagerRobot.Transition { fun openAddonsManagerMenu(interact: SettingsSubMenuAddonsManagerRobot.() -> Unit): SettingsSubMenuAddonsManagerRobot.Transition {
@ -384,13 +337,6 @@ class ThreeDotMenuMainRobot {
SettingsSubMenuAddonsManagerRobot().interact() SettingsSubMenuAddonsManagerRobot().interact()
return SettingsSubMenuAddonsManagerRobot.Transition() return SettingsSubMenuAddonsManagerRobot.Transition()
} }
fun exitSaveCollection(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
exitSaveCollectionButton().click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
} }
} }
private fun threeDotMenuRecyclerView() = private fun threeDotMenuRecyclerView() =
@ -417,10 +363,6 @@ private fun bookmarksButton() = onView(allOf(withText(R.string.library_bookmarks
private fun assertBookmarksButton() = bookmarksButton() private fun assertBookmarksButton() = bookmarksButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .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 signInToSyncButton() = onView(withText("Sign in to sync"))
private fun assertSignInToSyncButton() = signInToSyncButton().check(matches(isDisplayed())) 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 shareButton() = mDevice.findObject(UiSelector().description("Share"))
private fun assertShareButton() = assertTrue(shareButton().waitForExists(waitingTime)) 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 saveCollectionButton() = onView(allOf(withText("Save to collection"))).inRoot(RootMatchers.isPlatformPopup())
private fun assertSaveCollectionButton() = saveCollectionButton() private fun assertSaveCollectionButton() = saveCollectionButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
@ -472,16 +407,6 @@ private fun selectTabsButton() = onView(allOf(withText("Select tabs"))).inRoot(R
private fun assertSelectTabsButton() = selectTabsButton() private fun assertSelectTabsButton() = selectTabsButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .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 reportSiteIssueButton() = onView(withText("Report Site Issue…"))
private fun assertReportSiteIssueButton() = reportSiteIssueButton().check(matches(isDisplayed())) private fun assertReportSiteIssueButton() = reportSiteIssueButton().check(matches(isDisplayed()))
@ -581,15 +506,14 @@ private fun clickAddonsManagerButton() {
addOnsButton().check(matches(isCompletelyDisplayed())).click() addOnsButton().check(matches(isCompletelyDisplayed())).click()
} }
private fun exitSaveCollectionButton() = onView(withId(R.id.back_button)).check(matches(isDisplayed()))
private fun tabSettingsButton() = private fun tabSettingsButton() =
onView(allOf(withText("Tab settings"))).inRoot(RootMatchers.isPlatformPopup()) onView(allOf(withText("Tab settings"))).inRoot(RootMatchers.isPlatformPopup())
private fun assertTabSettingsButton() { private fun assertTabSettingsButton() {
tabSettingsButton() tabSettingsButton()
.check( .check(
matches(isDisplayed())) matches(isDisplayed())
)
} }
private fun recentlyClosedTabsButton() = private fun recentlyClosedTabsButton() =
@ -598,7 +522,8 @@ private fun recentlyClosedTabsButton() =
private fun assertRecentlyClosedTabsButton() { private fun assertRecentlyClosedTabsButton() {
recentlyClosedTabsButton() recentlyClosedTabsButton()
.check( .check(
matches(isDisplayed())) matches(isDisplayed())
)
} }
private fun shareAllTabsButton() = private fun shareAllTabsButton() =
@ -607,7 +532,8 @@ private fun shareAllTabsButton() =
private fun assertShareAllTabsButton() { private fun assertShareAllTabsButton() {
shareAllTabsButton() shareAllTabsButton()
.check( .check(
matches(isDisplayed())) matches(isDisplayed())
)
} }
private fun assertNewTabButton() = onView(withText("New tab")).check(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_ACCOUNT_SIGN_IN_HEADER = "Sync Firefox between devices"
const val STRING_ONBOARDING_TRACKING_PROTECTION_HEADER = "Always-on privacy" const val STRING_ONBOARDING_TRACKING_PROTECTION_HEADER = "Always-on privacy"
const val STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER = "Pick your toolbar placement" 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" <activity android:name=".autofill.AutofillUnlockActivity"
android:exported="false" android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar" /> android:theme="@style/Theme.AppCompat.Translucent" />
<activity android:name=".autofill.AutofillConfirmActivity" <activity android:name=".autofill.AutofillConfirmActivity"
android:exported="false" android:exported="false"
android:theme="@style/Theme.AppCompat.Translucent" /> android:theme="@style/Theme.AppCompat.Translucent" />
<activity android:name=".autofill.AutofillSearchActivity"
android:exported="false"
android:theme="@style/DialogActivityTheme" />
<service <service
android:name=".autofill.AutofillService" android:name=".autofill.AutofillService"
android:label="@string/app_name" android:label="@string/app_name"

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

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

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

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

@ -6,6 +6,7 @@ package org.mozilla.fenix
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.Intent.ACTION_MAIN
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Bundle 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.alreadyOnDestination
import org.mozilla.fenix.ext.breadcrumb import org.mozilla.fenix.ext.breadcrumb
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.measureNoInline import org.mozilla.fenix.ext.measureNoInline
import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav 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.ext.settings
import org.mozilla.fenix.home.HomeFragmentDirections import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.home.intent.CrashReporterIntentProcessor 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.OpenBrowserIntentProcessor
import org.mozilla.fenix.home.intent.OpenSpecificTabIntentProcessor import org.mozilla.fenix.home.intent.OpenSpecificTabIntentProcessor
import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor 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.bookmarks.DesktopFolders
import org.mozilla.fenix.library.history.HistoryFragmentDirections import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections 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.Performance
import org.mozilla.fenix.perf.PerformanceInflater import org.mozilla.fenix.perf.PerformanceInflater
import org.mozilla.fenix.perf.ProfilerMarkers 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.AddSearchEngineFragmentDirections
import org.mozilla.fenix.settings.search.EditCustomSearchEngineFragmentDirections import org.mozilla.fenix.settings.search.EditCustomSearchEngineFragmentDirections
import org.mozilla.fenix.share.AddNewDeviceFragmentDirections 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.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.DefaultThemeManager
import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.BrowsersCache import org.mozilla.fenix.utils.BrowsersCache
import org.mozilla.fenix.utils.Settings
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
/** /**
@ -160,7 +160,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
SpeechProcessingIntentProcessor(this, components.core.store, components.analytics.metrics), SpeechProcessingIntentProcessor(this, components.core.store, components.analytics.metrics),
StartSearchIntentProcessor(components.analytics.metrics), StartSearchIntentProcessor(components.analytics.metrics),
OpenBrowserIntentProcessor(this, ::getIntentSessionId), OpenBrowserIntentProcessor(this, ::getIntentSessionId),
OpenSpecificTabIntentProcessor(this) OpenSpecificTabIntentProcessor(this),
DefaultBrowserIntentProcessor(this, components.analytics.metrics)
) )
} }
@ -199,11 +200,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.publicSuffixList.prefetch() components.publicSuffixList.prefetch()
setContentView(R.layout.activity_home).run { setContentView(R.layout.activity_home)
// Do not call anything between setContentView and inflateNavGraphAsync.
// It needs to start its job as early as possible.
NavGraphProvider.inflateNavGraphAsync(navHost.navController, lifecycleScope)
}
// Must be after we set the content view // Must be after we set the content view
if (isVisuallyComplete) { if (isVisuallyComplete) {
@ -219,9 +216,12 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
it.start() it.start()
} }
if (isActivityColdStarted(intent, savedInstanceState) && if (!shouldStartOnHome() &&
!externalSourceIntentProcessors.any { it.process(intent, navHost.navController, this.intent) }) { shouldNavigateBrowserFragmentOnCouldStart(savedInstanceState)
) {
navigateToBrowserOnColdStart() navigateToBrowserOnColdStart()
} else if (FeatureFlags.showStartOnHomeSettings) {
components.analytics.metrics.track(Event.StartOnHomeEnterHomeScreen)
} }
Performance.processIntentIfPerformanceTest(intent, this) Performance.processIntentIfPerformanceTest(intent, this)
@ -238,10 +238,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
safeIntent safeIntent
?.let(::getIntentSource) ?.let(::getIntentSource)
?.also { components.analytics.metrics.track(Event.OpenedApp(it)) } ?.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() supportActionBar?.hide()
@ -257,7 +253,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
captureSnapshotTelemetryMetrics() captureSnapshotTelemetryMetrics()
startupTelemetryOnCreateCalled(intent.toSafeIntent(), savedInstanceState != null) startupTelemetryOnCreateCalled(intent.toSafeIntent())
startupPathProvider.attachOnActivityOnCreate(lifecycle, intent) startupPathProvider.attachOnActivityOnCreate(lifecycle, intent)
startupTypeTelemetry = StartupTypeTelemetry(components.startupStateProvider, startupPathProvider).apply { startupTypeTelemetry = StartupTypeTelemetry(components.startupStateProvider, startupPathProvider).apply {
attachOnHomeActivityOnCreate(lifecycle) attachOnHomeActivityOnCreate(lifecycle)
@ -268,17 +264,9 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
StartupTimeline.onActivityCreateEndHome(this) // DO NOT MOVE ANYTHING BELOW HERE. StartupTimeline.onActivityCreateEndHome(this) // DO NOT MOVE ANYTHING BELOW HERE.
} }
protected open fun startupTelemetryOnCreateCalled( private fun startupTelemetryOnCreateCalled(safeIntent: SafeIntent) {
safeIntent: SafeIntent, // We intentionally only record this in HomeActivity and not ExternalBrowserActivity (e.g.
hasSavedInstanceState: Boolean // PWAs) so we don't include more unpredictable code paths in the results.
) {
// This function gets overridden by subclasses.
components.appStartupTelemetry.onHomeActivityOnCreate(
safeIntent,
hasSavedInstanceState,
homeActivityInitTimeStampNanoSeconds, rootContainer
)
components.performance.coldStartupDurationTelemetry.onHomeActivityOnCreate( components.performance.coldStartupDurationTelemetry.onHomeActivityOnCreate(
components.performance.visualCompletenessQueue, components.performance.visualCompletenessQueue,
components.startupStateProvider, 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 @CallSuper
override fun onResume() { override fun onResume() {
super.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 isFenixTheDefaultBrowser()
lifecycleScope.launch(IO) {
if (
settings().isDefaultBrowser() &&
settings().wasDefaultBrowserOnLastResume != settings().isDefaultBrowser()
) {
metrics.track(Event.ChangedToDefaultBrowser)
}
settings().wasDefaultBrowserOnLastResume = settings().isDefaultBrowser()
}
} }
override fun onStart() = PerfStartup.homeActivityOnStart.measureNoInline { override fun onStart() = PerfStartup.homeActivityOnStart.measureNoInline {
@ -366,8 +333,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
"finishing" to isFinishing.toString() "finishing" to isFinishing.toString()
) )
) )
components.appStartupTelemetry.onStop()
} }
final override fun onPause() { final override fun onPause() {
@ -502,19 +467,9 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
?.childFragmentManager ?.childFragmentManager
?.fragments ?.fragments
?.lastOrNull() ?.lastOrNull()
?.let { it as? TabTrayDialogFragment } ?.let { it as? TabsTrayFragment }
?.also { it.dismissAllowingStateLoss() } ?.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() super.onBackPressed()
} }
@Suppress("DEPRECATION")
// https://github.com/mozilla-mobile/fenix/issues/19919
final override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { final override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach { supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach {
if (it is ActivityResultHandler && it.onActivityResult(requestCode, data, resultCode)) { 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 * 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 * 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) SearchDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromSettings -> BrowserDirection.FromSettings ->
SettingsFragmentDirections.actionGlobalBrowser(customTabSessionId) SettingsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromSyncedTabs ->
SyncedTabsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromBookmarks -> BrowserDirection.FromBookmarks ->
BookmarkFragmentDirections.actionGlobalBrowser(customTabSessionId) BookmarkFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromHistory -> BrowserDirection.FromHistory ->
@ -813,9 +760,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
AddonPermissionsDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId) AddonPermissionsDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromLoginDetailFragment -> BrowserDirection.FromLoginDetailFragment ->
LoginDetailFragmentDirections.actionGlobalBrowser(customTabSessionId) LoginDetailFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTabTrayDialog -> BrowserDirection.FromTabsTray ->
TabTrayDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTabTray ->
TabsTrayFragmentDirections.actionGlobalBrowser(customTabSessionId) TabsTrayFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromRecentlyClosed -> BrowserDirection.FromRecentlyClosed ->
RecentlyClosedFragmentDirections.actionGlobalBrowser(customTabSessionId) RecentlyClosedFragmentDirections.actionGlobalBrowser(customTabSessionId)
@ -837,28 +782,38 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
val startTime = components.core.engine.profiler?.getProfilerTime() val startTime = components.core.engine.profiler?.getProfilerTime()
val mode = browsingModeManager.mode val mode = browsingModeManager.mode
val loadUrlUseCase = if (newTab) { val private = when (mode) {
when (mode) { BrowsingMode.Private -> true
BrowsingMode.Private -> components.useCases.tabsUseCases.addPrivateTab BrowsingMode.Normal -> false
BrowsingMode.Normal -> components.useCases.tabsUseCases.addTab }
}
} else components.useCases.sessionUseCases.loadUrl
// In situations where we want to perform a search but have no search engine (e.g. the user // 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 // 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. // and let it try to load whatever was entered.
if ((!forceSearch && searchTermOrURL.isUrl()) || engine == null) { 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) { if (requestDesktopMode && tabId != null) {
handleRequestDesktopMode() handleRequestDesktopMode(tabId)
} }
} else { } else {
if (newTab) { if (newTab) {
components.useCases.searchUseCases.newTabSearch components.useCases.searchUseCases.newTabSearch
.invoke( .invoke(
searchTermOrURL, searchTermOrURL,
SessionState.Source.USER_ENTERED, SessionState.Source.Internal.UserEntered,
true, true,
mode.isPrivate, mode.isPrivate,
searchEngine = engine searchEngine = engine
@ -879,15 +834,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
} }
} }
internal fun handleRequestDesktopMode() { internal fun handleRequestDesktopMode(tabId: String) {
val requestDesktopSiteUseCase = components.useCases.sessionUseCases.requestDesktopSite(true, tabId)
components.useCases.sessionUseCases.requestDesktopSite components.core.store.dispatch(ContentAction.UpdateDesktopModeAction(tabId, true))
requestDesktopSiteUseCase.invoke(true)
components.core.store.dispatch(
ContentAction.UpdateDesktopModeAction(
components.core.store.state.selectedTabId.toString(), true
)
)
// Reset preference value after opening the tab in desktop mode // Reset preference value after opening the tab in desktop mode
settings().openNextTabInDesktopMode = false settings().openNextTabInDesktopMode = false
} }
@ -948,7 +898,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
webExtensionId = webExtensionState.id, webExtensionId = webExtensionState.id,
webExtensionTitle = webExtensionState.name 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 @VisibleForTesting
internal fun isActivityColdStarted(startingIntent: Intent, activityIcicle: Bundle?): Boolean { internal fun isActivityColdStarted(startingIntent: Intent, activityIcicle: Bundle?): Boolean {
// First time opening this activity in the task. // First time opening this activity in the task.
// Cold start / start from Recents after back press. // Cold start / start from Recents after back press.
return activityIcicle == null && return activityIcicle == null &&
// Activity was restarted from Recents after it was destroyed by Android while in background // Activity was restarted from Recents after it was destroyed by Android while in background
// in cases of memory pressure / "Don't keep activities". // in cases of memory pressure / "Don't keep activities".
startingIntent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY == 0 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 { companion object {

@ -6,10 +6,14 @@ package org.mozilla.fenix
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.StrictMode import android.os.StrictMode
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import mozilla.components.feature.intent.processing.IntentProcessor 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.HomeActivity.Companion.PRIVATE_BROWSING_MODE
import org.mozilla.fenix.components.IntentProcessorType import org.mozilla.fenix.components.IntentProcessorType
import org.mozilla.fenix.components.getType import org.mozilla.fenix.components.getType
@ -54,6 +58,8 @@ class IntentReceiverActivity : Activity() {
components.analytics.metrics.track(Event.OpenedLink(Event.OpenedLink.Mode.NORMAL)) components.analytics.metrics.track(Event.OpenedLink(Event.OpenedLink.Mode.NORMAL))
} }
addReferrerInformation(intent)
val processor = getIntentProcessors(private).firstOrNull { it.process(intent) } val processor = getIntentProcessors(private).firstOrNull { it.process(intent) }
val intentProcessorType = components.intentProcessors.getType(processor) val intentProcessorType = components.intentProcessors.getType(processor)
@ -96,6 +102,28 @@ class IntentReceiverActivity : Activity() {
modeDependentProcessors + modeDependentProcessors +
NewTabShortcutIntentProcessor() 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() { private fun Intent.stripUnwantedFlags() {

@ -18,7 +18,8 @@ import org.mozilla.fenix.ext.showToolbar
/** /**
* A fragment to show the permissions of an add-on. * 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 { AddonPermissionsDetailsInteractor {
private val args by navArgs<AddonPermissionsDetailsFragmentArgs>() private val args by navArgs<AddonPermissionsDetailsFragmentArgs>()

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

@ -8,7 +8,6 @@ import androidx.navigation.NavController
import mozilla.components.feature.addons.Addon import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.ui.AddonsManagerAdapterDelegate import mozilla.components.feature.addons.ui.AddonsManagerAdapterDelegate
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.navigateSafe import org.mozilla.fenix.ext.navigateSafe
/** /**
@ -56,6 +55,6 @@ class AddonsManagementView(
AddonsManagementFragmentDirections.actionAddonsManagementFragmentToNotYetSupportedAddonFragment( AddonsManagementFragmentDirections.actionAddonsManagementFragmentToNotYetSupportedAddonFragment(
unsupportedAddons.toTypedArray() 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.R
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.ext.runIfFragmentIsAttached import org.mozilla.fenix.ext.runIfFragmentIsAttached
@ -205,18 +204,14 @@ class InstalledAddonDetailsFragment : Fragment() {
val shouldCreatePrivateSession = val shouldCreatePrivateSession =
(activity as HomeActivity).browsingModeManager.mode.isPrivate (activity as HomeActivity).browsingModeManager.mode.isPrivate
if (shouldCreatePrivateSession) { components.useCases.tabsUseCases.addTab(settingUrl, private = shouldCreatePrivateSession)
components.useCases.tabsUseCases.addPrivateTab(settingUrl)
} else {
components.useCases.tabsUseCases.addTab(settingUrl)
}
InstalledAddonDetailsFragmentDirections.actionGlobalBrowser(null) InstalledAddonDetailsFragmentDirections.actionGlobalBrowser(null)
} else { } else {
InstalledAddonDetailsFragmentDirections InstalledAddonDetailsFragmentDirections
.actionInstalledAddonFragmentToAddonInternalSettingsFragment(addon) .actionInstalledAddonFragmentToAddonInternalSettingsFragment(addon)
} }
Navigation.findNavController(this).navigateBlockingForAsyncNavGraph(directions) Navigation.findNavController(this).navigate(directions)
} }
} }
} }
@ -227,7 +222,7 @@ class InstalledAddonDetailsFragment : Fragment() {
InstalledAddonDetailsFragmentDirections.actionInstalledAddonFragmentToAddonDetailsFragment( InstalledAddonDetailsFragmentDirections.actionInstalledAddonFragmentToAddonDetailsFragment(
addon addon
) )
Navigation.findNavController(view).navigateBlockingForAsyncNavGraph(directions) Navigation.findNavController(view).navigate(directions)
} }
} }
@ -237,7 +232,7 @@ class InstalledAddonDetailsFragment : Fragment() {
InstalledAddonDetailsFragmentDirections.actionInstalledAddonFragmentToAddonPermissionsDetailsFragment( InstalledAddonDetailsFragmentDirections.actionInstalledAddonFragmentToAddonPermissionsDetailsFragment(
addon 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.PictureInPictureFeature
import mozilla.components.feature.session.SessionFeature import mozilla.components.feature.session.SessionFeature
import mozilla.components.feature.session.SwipeRefreshFeature 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.feature.sitepermissions.SitePermissionsFeature
import mozilla.components.lib.state.ext.consumeFlow import mozilla.components.lib.state.ext.consumeFlow
import mozilla.components.lib.state.ext.flowScoped 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.metrics.Event
import org.mozilla.fenix.components.toolbar.BrowserFragmentState import org.mozilla.fenix.components.toolbar.BrowserFragmentState
import org.mozilla.fenix.components.toolbar.BrowserFragmentStore 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.BrowserToolbarView
import org.mozilla.fenix.components.toolbar.BrowserToolbarViewInteractor
import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarController import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarController
import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarMenuController import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarMenuController
import org.mozilla.fenix.components.toolbar.ToolbarIntegration 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.session.behavior.EngineViewBrowserToolbarBehavior
import mozilla.components.feature.webauthn.WebAuthnFeature import mozilla.components.feature.webauthn.WebAuthnFeature
import mozilla.components.support.base.feature.ActivityResultHandler 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.android.view.enterToImmersiveMode
import mozilla.components.support.ktx.kotlin.getOrigin import mozilla.components.support.ktx.kotlin.getOrigin
import org.mozilla.fenix.GleanMetrics.PerfStartup 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.measureNoInline
import org.mozilla.fenix.ext.secure import org.mozilla.fenix.ext.secure
import org.mozilla.fenix.settings.biometric.BiometricPromptFeature import org.mozilla.fenix.settings.biometric.BiometricPromptFeature
@ -149,15 +148,19 @@ import mozilla.components.feature.session.behavior.ToolbarPosition as MozacToolb
*/ */
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
@Suppress("TooManyFunctions", "LargeClass") @Suppress("TooManyFunctions", "LargeClass")
abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, ActivityResultHandler, abstract class BaseBrowserFragment :
OnBackLongPressedListener, AccessibilityManager.AccessibilityStateChangeListener { Fragment(),
UserInteractionHandler,
ActivityResultHandler,
OnBackLongPressedListener,
AccessibilityManager.AccessibilityStateChangeListener {
private lateinit var browserFragmentStore: BrowserFragmentStore private lateinit var browserFragmentStore: BrowserFragmentStore
private lateinit var browserAnimator: BrowserAnimator private lateinit var browserAnimator: BrowserAnimator
private var _browserInteractor: BrowserToolbarViewInteractor? = null private var _browserToolbarInteractor: BrowserToolbarInteractor? = null
protected val browserInteractor: BrowserToolbarViewInteractor protected val browserToolbarInteractor: BrowserToolbarInteractor
get() = _browserInteractor!! get() = _browserToolbarInteractor!!
@VisibleForTesting @VisibleForTesting
@Suppress("VariableNaming") @Suppress("VariableNaming")
@ -236,25 +239,25 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
} }
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) = final override fun onViewCreated(view: View, savedInstanceState: Bundle?) =
PerfStartup.baseBfragmentOnViewCreated.measureNoInline { // weird indentation to avoid breaking blame. PerfStartup.baseBfragmentOnViewCreated.measureNoInline { // weird indentation to avoid breaking blame.
initializeUI(view) initializeUI(view)
if (customTabSessionId == null) { if (customTabSessionId == null) {
// We currently only need this observer to navigate to home // We currently only need this observer to navigate to home
// in case all tabs have been removed on startup. No need to // in case all tabs have been removed on startup. No need to
// this if we have a known session to display. // this if we have a known session to display.
observeRestoreComplete(requireComponents.core.store, findNavController()) observeRestoreComplete(requireComponents.core.store, findNavController())
} }
observeTabSelection(requireComponents.core.store) observeTabSelection(requireComponents.core.store)
if (!onboarding.userHasBeenOnboarded()) { if (!onboarding.userHasBeenOnboarded()) {
observeTabSource(requireComponents.core.store) observeTabSource(requireComponents.core.store)
} }
requireContext().accessibilityManager.addAccessibilityStateChangeListener(this) requireContext().accessibilityManager.addAccessibilityStateChangeListener(this)
Unit Unit
} }
private fun initializeUI(view: View) { private fun initializeUI(view: View) {
val tab = getCurrentTab() 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 @CallSuper
internal open fun initializeUI(view: View, tab: SessionState) { internal open fun initializeUI(view: View, tab: SessionState) {
val context = requireContext() val context = requireContext()
@ -309,7 +313,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
thumbnailsFeature.get()?.requestScreenshot() thumbnailsFeature.get()?.requestScreenshot()
findNavController().nav( findNavController().nav(
R.id.browserFragment, R.id.browserFragment,
getTrayDirection(context) BrowserFragmentDirections.actionGlobalTabsTrayFragment()
) )
}, },
onCloseTab = { closedSession -> onCloseTab = { closedSession ->
@ -357,7 +361,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
browserStore = store browserStore = store
) )
_browserInteractor = BrowserInteractor( _browserToolbarInteractor = DefaultBrowserToolbarInteractor(
browserToolbarController, browserToolbarController,
browserToolbarMenuController browserToolbarMenuController
) )
@ -365,7 +369,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
_browserToolbarView = BrowserToolbarView( _browserToolbarView = BrowserToolbarView(
container = view.browserLayout, container = view.browserLayout,
toolbarPosition = context.settings().toolbarPosition, toolbarPosition = context.settings().toolbarPosition,
interactor = browserInteractor, interactor = browserToolbarInteractor,
customTabSession = customTabSessionId?.let { store.state.findCustomTab(it) }, customTabSession = customTabSessionId?.let { store.state.findCustomTab(it) },
lifecycleOwner = viewLifecycleOwner lifecycleOwner = viewLifecycleOwner
) )
@ -420,7 +424,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
window = requireActivity().window, window = requireActivity().window,
store = store, store = store,
customTabId = customTabSessionId, customTabId = customTabSessionId,
isSecure = { !allowScreenshotsInPrivateMode && it.content.private } isSecure = { !allowScreenshotsInPrivateMode && it.content.private },
clearFlagOnStop = false
), ),
owner = this, owner = this,
view = view view = view
@ -489,16 +494,14 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
val dynamicDownloadDialog = DynamicDownloadDialog( val dynamicDownloadDialog = DynamicDownloadDialog(
container = view.browserLayout, container = view.browserLayout,
downloadState = downloadState, downloadState = downloadState,
metrics = requireComponents.analytics.metrics,
didFail = downloadJobStatus == DownloadState.Status.FAILED, didFail = downloadJobStatus == DownloadState.Status.FAILED,
tryAgain = downloadFeature::tryAgain, tryAgain = downloadFeature::tryAgain,
onCannotOpenFile = { onCannotOpenFile = {
showCannotOpenFileError(view.browserLayout, context, it) showCannotOpenFileError(view.browserLayout, context, it)
}, },
view = view.viewDynamicDownloadDialog, view = view.viewDynamicDownloadDialog,
toolbarHeight = toolbarHeight, toolbarHeight = toolbarHeight
onDismiss = { sharedViewModel.downloadDialogState.remove(downloadState.sessionId) } ) { sharedViewModel.downloadDialogState.remove(downloadState.sessionId) }
)
dynamicDownloadDialog.show() dynamicDownloadDialog.show()
browserToolbarView.expand() browserToolbarView.expand()
@ -585,7 +588,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
showPage = true, showPage = true,
sessionId = getCurrentTab()?.id sessionId = getCurrentTab()?.id
) )
findNavController().navigateBlockingForAsyncNavGraph(directions) findNavController().navigate(directions)
} }
}, },
onNeedToRequestPermissions = { permissions -> onNeedToRequestPermissions = { permissions ->
@ -596,14 +599,14 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
browserAnimator.captureEngineViewAndDrawStatically { browserAnimator.captureEngineViewAndDrawStatically {
val directions = val directions =
NavGraphDirections.actionGlobalSavedLoginsAuthFragment() NavGraphDirections.actionGlobalSavedLoginsAuthFragment()
findNavController().navigateBlockingForAsyncNavGraph(directions) findNavController().navigate(directions)
} }
}, },
creditCardPickerView = creditCardSelectBar, creditCardPickerView = creditCardSelectBar,
onManageCreditCards = { onManageCreditCards = {
val directions = val directions =
NavGraphDirections.actionGlobalCreditCardsSettingFragment() NavGraphDirections.actionGlobalCreditCardsSettingFragment()
findNavController().navigateBlockingForAsyncNavGraph(directions) findNavController().navigate(directions)
}, },
onSelectCreditCard = { onSelectCreditCard = {
showBiometricPrompt(context) showBiometricPrompt(context)
@ -650,7 +653,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
sitePermissionsFeature.set( sitePermissionsFeature.set(
feature = SitePermissionsFeature( feature = SitePermissionsFeature(
context = context, context = context,
storage = context.components.core.permissionStorage.permissionsStorage, storage = context.components.core.geckoSitePermissionsStorage,
fragmentManager = parentFragmentManager, fragmentManager = parentFragmentManager,
promptsStyling = SitePermissionsFeature.PromptsStyling( promptsStyling = SitePermissionsFeature.PromptsStyling(
gravity = getAppropriateLayoutGravity(), gravity = getAppropriateLayoutGravity(),
@ -822,15 +825,17 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
internal fun expandToolbarOnNavigation(store: BrowserStore) { internal fun expandToolbarOnNavigation(store: BrowserStore) {
consumeFlow(store) { flow -> consumeFlow(store) { flow ->
flow.mapNotNull { flow.mapNotNull {
state -> state.findCustomTabOrSelectedTab(customTabSessionId) state ->
} state.findCustomTabOrSelectedTab(customTabSessionId)
.ifAnyChanged {
tab -> arrayOf(tab.content.url, tab.content.loadRequest)
}
.collect {
findInPageIntegration.onBackPressed()
browserToolbarView.expand()
} }
.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( DynamicDownloadDialog(
container = view.browserLayout, container = view.browserLayout,
downloadState = savedDownloadState.first, downloadState = savedDownloadState.first,
metrics = requireComponents.analytics.metrics,
didFail = savedDownloadState.second, didFail = savedDownloadState.second,
tryAgain = onTryAgain, tryAgain = onTryAgain,
onCannotOpenFile = { onCannotOpenFile = {
@ -906,8 +910,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
@VisibleForTesting @VisibleForTesting
internal fun shouldPullToRefreshBeEnabled(inFullScreen: Boolean): Boolean { internal fun shouldPullToRefreshBeEnabled(inFullScreen: Boolean): Boolean {
return FeatureFlags.pullToRefreshEnabled && return FeatureFlags.pullToRefreshEnabled &&
requireContext().settings().isPullToRefreshEnabledInBrowser && requireContext().settings().isPullToRefreshEnabledInBrowser &&
!inFullScreen !inFullScreen
} }
@VisibleForTesting @VisibleForTesting
@ -980,12 +984,12 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
flow.ifChanged { flow.ifChanged {
it.selectedTabId it.selectedTabId
} }
.mapNotNull { .mapNotNull {
it.selectedTab it.selectedTab
} }
.collect { .collect {
handleTabSelected(it) handleTabSelected(it)
} }
} }
} }
@ -997,14 +1001,14 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
state.selectedTab state.selectedTab
} }
.collect { .collect {
if (!onboarding.userHasBeenOnboarded() && if (!onboarding.userHasBeenOnboarded() &&
it.content.loadRequest?.triggeredByRedirect != true && it.content.loadRequest?.triggeredByRedirect != true &&
it.source !in intentSourcesList && it.source !is SessionState.Source.External &&
it.content.url !in onboardingLinksList it.content.url !in onboardingLinksList
) { ) {
onboarding.finish() onboarding.finish()
}
} }
}
} }
} }
@ -1069,14 +1073,14 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
@CallSuper @CallSuper
override fun onBackPressed(): Boolean { override fun onBackPressed(): Boolean {
return findInPageIntegration.onBackPressed() || return findInPageIntegration.onBackPressed() ||
fullScreenFeature.onBackPressed() || fullScreenFeature.onBackPressed() ||
promptsFeature.onBackPressed() || promptsFeature.onBackPressed() ||
sessionFeature.onBackPressed() || sessionFeature.onBackPressed() ||
removeSessionIfNeeded() removeSessionIfNeeded()
} }
override fun onBackLongPressed(): Boolean { override fun onBackLongPressed(): Boolean {
findNavController().navigateBlockingForAsyncNavGraph( findNavController().navigate(
NavGraphDirections.actionGlobalTabHistoryDialogFragment( NavGraphDirections.actionGlobalTabHistoryDialogFragment(
activeSessionId = customTabSessionId activeSessionId = customTabSessionId
) )
@ -1137,7 +1141,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
*/ */
protected open fun removeSessionIfNeeded(): Boolean { protected open fun removeSessionIfNeeded(): Boolean {
getCurrentTab()?.let { session -> getCurrentTab()?.let { session ->
return if (session.source == SessionState.Source.ACTION_VIEW) { return if (session.source is SessionState.Source.External && !session.restored) {
activity?.finish() activity?.finish()
requireComponents.useCases.tabsUseCases.removeTab(session.id) requireComponents.useCases.tabsUseCases.removeTab(session.id)
true true
@ -1186,7 +1190,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
val tab = getCurrentTab() ?: return val tab = getCurrentTab() ?: return
viewLifecycleOwner.lifecycleScope.launch(Main) { viewLifecycleOwner.lifecycleScope.launch(Main) {
val sitePermissions: SitePermissions? = tab.content.url.getOrigin()?.let { origin -> val sitePermissions: SitePermissions? = tab.content.url.getOrigin()?.let { origin ->
val storage = requireComponents.core.permissionStorage val storage = requireComponents.core.permissionStorage
storage.findSitePermissionsBy(origin) storage.findSitePermissionsBy(origin)
} }
@ -1343,7 +1347,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
requireContext().accessibilityManager.removeAccessibilityStateChangeListener(this) requireContext().accessibilityManager.removeAccessibilityStateChangeListener(this)
_browserToolbarView = null _browserToolbarView = null
_browserInteractor = null _browserToolbarInteractor = null
} }
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
@ -1379,17 +1383,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
.show() .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 { companion object {
private const val KEY_CUSTOM_TAB_SESSION_ID = "custom_tab_session_id" private const val KEY_CUSTOM_TAB_SESSION_ID = "custom_tab_session_id"
private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 1 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.getMozillaPageUrl(SupportUtils.MozillaPage.PRIVATE_NOTICE),
SupportUtils.getFirefoxAccountSumoUrl() 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) { override fun onAccessibilityStateChanged(enabled: Boolean) {

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

@ -28,7 +28,8 @@ class FenixSnackbarDelegate(private val view: View) : ContextMenuCandidate.Snack
.setAction(view.context.getString(action)) { listener.invoke(view) } .setAction(view.context.getString(action)) { listener.invoke(view) }
.show() .show()
} else { } else {
FenixSnackbar.make(view, FenixSnackbar.make(
view,
duration = FenixSnackbar.LENGTH_SHORT, duration = FenixSnackbar.LENGTH_SHORT,
isDisplayedWithBrowserToolbar = true isDisplayedWithBrowserToolbar = true
) )

@ -24,9 +24,6 @@ import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.browser.infobanner.DynamicInfoBanner import org.mozilla.fenix.browser.infobanner.DynamicInfoBanner
import org.mozilla.fenix.browser.infobanner.InfoBanner import org.mozilla.fenix.browser.infobanner.InfoBanner
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.Event.BannerOpenInAppGoToSettings
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
@ -58,22 +55,23 @@ class OpenInAppOnboardingObserver(
flow.mapNotNull { state -> flow.mapNotNull { state ->
state.selectedTab state.selectedTab
} }
.ifAnyChanged { .ifAnyChanged {
tab -> arrayOf(tab.content.url, tab.content.loading) tab ->
} arrayOf(tab.content.url, tab.content.loading)
.collect { tab -> }
if (tab.content.url != currentUrl) { .collect { tab ->
sessionDomainForDisplayedBanner?.let { if (tab.content.url != currentUrl) {
if (tab.content.url.tryGetHostFromUrl() != it) { sessionDomainForDisplayedBanner?.let {
infoBanner?.dismiss() if (tab.content.url.tryGetHostFromUrl() != it) {
infoBanner?.dismiss()
}
} }
currentUrl = tab.content.url
} else {
// Loading state has changed
maybeShowOpenInAppBanner(tab.content.url, tab.content.loading)
} }
currentUrl = tab.content.url
} else {
// Loading state has changed
maybeShowOpenInAppBanner(tab.content.url, tab.content.loading)
} }
}
} }
} }
@ -92,7 +90,6 @@ class OpenInAppOnboardingObserver(
infoBanner?.showBanner() infoBanner?.showBanner()
sessionDomainForDisplayedBanner = url.tryGetHostFromUrl() sessionDomainForDisplayedBanner = url.tryGetHostFromUrl()
settings.shouldShowOpenInAppBanner = false settings.shouldShowOpenInAppBanner = false
context.components.analytics.metrics.track(Event.BannerOpenInAppDisplayed)
} }
} }
@ -104,18 +101,12 @@ class OpenInAppOnboardingObserver(
dismissText = context.getString(R.string.open_in_app_cfr_negative_button_text), dismissText = context.getString(R.string.open_in_app_cfr_negative_button_text),
actionText = context.getString(R.string.open_in_app_cfr_positive_button_text), actionText = context.getString(R.string.open_in_app_cfr_positive_button_text),
container = container, container = container,
shouldScrollWithTopToolbar = shouldScrollWithTopToolbar, shouldScrollWithTopToolbar = shouldScrollWithTopToolbar
dismissAction = ::dismissAction
) { ) {
val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment( val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment(
preferenceToScrollTo = context.getString(R.string.pref_key_open_links_in_external_app) preferenceToScrollTo = context.getString(R.string.pref_key_open_links_in_external_app)
) )
context.components.analytics.metrics.track(BannerOpenInAppGoToSettings)
navController.nav(R.id.browserFragment, directions) navController.nav(R.id.browserFragment, directions)
} }
} }
private fun dismissAction() {
context.components.analytics.metrics.track(Event.BannerOpenInAppDismissed)
}
} }

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

Loading…
Cancel
Save