v1-t
harvey186 2024-12-12 08:45:34 +01:00
commit 37a045067d
1932 changed files with 410378 additions and 0 deletions

30
.cron.yml Normal file
View File

@ -0,0 +1,30 @@
# Definitions for jobs that run periodically. For details on the format, see
# `taskcluster/taskgraph/cron/schema.py`. For documentation, see
# `taskcluster/docs/cron.rst`.
---
jobs:
- name: fennec-production
job:
type: decision-task
treeherder-symbol: fennec-production
target-tasks-method: fennec-production
when: [] # Force hook only
- name: bump-android-components
job:
type: decision-task
treeherder-symbol: bump-ac
target-tasks-method: bump_android_components
when: [{hour: 15, minute: 30}]
- name: screenshots
job:
type: decision-task
treeherder-symbol: screenshots-D
target-tasks-method: screenshots
when: [{weekday: 'Monday', hour: 10, minute: 0}]
- name: legacy-api-ui-tests
job:
type: decision-task
treeherder-symbol: legacy-api-ui
target-tasks-method: legacy_api_ui_tests
when: [{hour: 10, minute: 30}]

11
.editorconfig Normal file
View File

@ -0,0 +1,11 @@
[*.{kt,kts}]
# Disabling rules that were added in the latest versions of ktlint
# tracking here: https://github.com/mozilla-mobile/fenix/issues/4861
ktlint_disabled_rules=import-ordering
ij_kotlin_allow_trailing_comma_on_call_site=true
ij_kotlin_allow_trailing_comma=true
[*]
insert_final_newline = true

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
CHANGELOG.md merge=union

102
.gitignore vendored Normal file
View File

@ -0,0 +1,102 @@
# Created by https://www.gitignore.io/api/android
# Edit at https://www.gitignore.io/?templates=android
### Android ###
# Built application files
*.apk
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/
# Vim swap files
*.sw[op]
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
# Google Services (e.g. APIs or Firebase)
google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
### Android Patch ###
gen-external-apklibs
# End of https://www.gitignore.io/api/android
# macOS
.DS_Store
# Secrets files, e.g. tokens
.sentry_token
.mls_token
.wallpaper_url
# Python Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
venv/
# UI test artifacts
.firebase_token*
results/
test_artifacts/
/build/test-tools/google-cloud-sdk/
/build/test-tools/*.jar
/build/test-tools/*.gz
# Web extensions: manifest.json files are generated
manifest.json

100
.mergify.yml Normal file
View File

@ -0,0 +1,100 @@
queue_rules:
- name: default
conditions:
- or:
- status-success=complete-pr
- and:
# For more context, see "Auto Merge" rules down below
- base~=^releases[_/].*
- status-success=complete-push
- or:
- head~=^automation/sync-strings-\d+
- head~=^relbot/fenix-\d+
pull_request_rules:
- name: Resolve conflict
conditions:
- conflict
actions:
comment:
message: This pull request has conflicts when rebasing. Could you fix it @{{author}}? 🙏
- name: Android-Components bump - Auto Merge
conditions:
- and:
- files=buildSrc/src/main/java/AndroidComponents.kt
- -files~=^(?!buildSrc/src/main/java/AndroidComponents.kt).+$
- or:
- and:
- author=MickeyMoz
- base=main
- head=ac-update
- status-success=complete-pr
- and:
- author=github-actions[bot]
- base~=^releases[_/].*
- head~=^relbot/fenix-\d+
- status-success=complete-push
actions:
review:
type: APPROVE
message: 🚢
queue:
method: rebase
name: default
rebase_fallback: none
- name: L10N - Auto Merge
conditions:
- and:
- files~=^(l10n.toml|app/src/main/res/values[A-Za-z-]*/strings\.xml)$
# /!\ The line above doesn't prevent random files to be changed alongside
# l10n ones. That's why the additional condition exists below. For more
# information: https://docs.mergify.com/conditions/#how-to-match-lists
- -files~=^(?!(l10n.toml|app/src/main/res/values[A-Za-z-]*/strings\.xml)).+$
- or:
- and:
- author=mozilla-l10n-automation-bot
- base=main
- head=import-l10n
- status-success=complete-pr
- and:
- author=github-actions[bot]
- base~=^releases[_/].*
- head~=^automation/sync-strings-\d+
- status-success=complete-push
# Taskcluster only runs on git-push events because github-actions[bot] is not considered
# a collaborator, so pull request events are triggered. That said, github-actions[bot]
# doesn't create the PR on a separate fork (unlike mozilla-l10n-automation-bot). That's
# why git-push events are taken into account
actions:
review:
type: APPROVE
message: LGTM 😎
queue:
method: rebase
name: default
rebase_fallback: none
- name: Needs landing - Rebase
conditions:
- check-success=complete-pr
- label=pr:needs-landing
- "#approved-reviews-by>=1"
- -draft
- label!=pr:work-in-progress
- label!=pr:do-not-land
actions:
queue:
method: rebase
name: default
rebase_fallback: none
- name: Needs landing - Squash
conditions:
- check-success=complete-pr
- label=pr:needs-landing-squashed
- "#approved-reviews-by>=1"
- -draft
- label!=pr:work-in-progress
- label!=pr:do-not-land
actions:
queue:
method: squash
name: default
rebase_fallback: none

15
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,15 @@
# Community Participation Guidelines
This repository is governed by Mozilla's code of conduct and etiquette guidelines.
For more details, please read the
[Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/).
## How to Report
For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page.
<!--
## Project Specific Etiquette
In some cases, there will be additional project etiquette i.e.: (https://bugzilla.mozilla.org/page.cgi?id=etiquette.html).
Please update for your project.
-->

3
Gemfile Normal file
View File

@ -0,0 +1,3 @@
source "https://rubygems.org"
gem "fastlane"

53
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,53 @@
pipeline {
agent any
triggers {
cron(env.BRANCH_NAME == 'main' ? 'H 0 * * *' : '')
}
options {
timestamps()
timeout(time: 1, unit: 'HOURS')
}
stages {
stage('test') {
when { branch 'main' }
steps {
dir('app/src/androidTest/java/net/waterfox/android/syncIntegration') {
sh 'pipenv install'
sh 'pipenv check'
sh 'pipenv run pytest'
}
}
}
}
post {
always {
script {
if (env.BRANCH_NAME == 'main') {
publishHTML(target: [
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'app/src/androidTest/java/net/waterfox/android/syncintegration/results',
reportFiles: 'index.html',
reportName: 'HTML Report'])
}
}
}
failure {
script {
if (env.BRANCH_NAME == 'main') {
slackSend(
color: 'danger',
message: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL}HTML_20Report/)")
}
}
}
fixed {
slackSend(
color: 'good',
message: "FIXED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL}HTML_20Report/)")
}
}
}

373
LICENSE Normal file
View File

@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
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/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

10
README.md Normal file
View File

@ -0,0 +1,10 @@
# LeOSium
The all-new LeOSium Browser ist based on Waterfox. On LeOSium all firebase trackers are removed.
## License
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/

5
SECURITY.md Normal file
View File

@ -0,0 +1,5 @@
# Security Policy
## Reporting a Vulnerability
Report all security vunerablites to [Bugzilla Fenix::Security](https://bugzilla.mozilla.org/enter_bug.cgi?product=Fenix&component=Security). If they are not a security bug you will be asked to move your report to [Fenix GitHub](https://github.com/mozilla-mobile/fenix/issues). See the [Mozilla Security Bug Bounty Program](https://www.mozilla.org/en-US/security/bug-bounty/) and the [client security reporting](https://www.mozilla.org/en-US/security/client-bug-bounty/) pages for details. In any case where this document and the Mozilla.org pages differ the Mozilla.org pages are the official documentation.

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

81
app/benchmark.gradle Normal file
View File

@ -0,0 +1,81 @@
/* 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=net.waterfox.android.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>/net.waterfox.android-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 "release"
// 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.
testInstrumentationRunnerArguments = [
// - 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.
'androidx.benchmark.suppressErrors' : 'ACTIVITY-MISSING,UNLOCKED',
// The tests don't always output a JSON file with the data. To make sure it does, we have to
// set androidx.benchmark.output.enable to true.
'androidx.benchmark.output.enable' : 'true',
// We set the the output directory simply for simplicity since the benchmark_runner.py script
// can't know the name of the phone in the /build/outputs/ directory. The system defaults to
// {phone_name} which can be troublesome finding in some case.
//
// NOTE: Jetpack Benchmark outputs to Logcat too. However, the output in the logcat is
// the min of the several repeats, for more statistics. Therefore, to get more stats,
// we refer to the JSON file.
additionalTestOutputDir : '/storage/emulated/0/benchmark'
]
}
}

685
app/build.gradle Normal file
View File

@ -0,0 +1,685 @@
import com.android.build.OutputFile
import groovy.json.JsonOutput
import net.waterfox.android.gradle.tasks.ApkSizeTask
plugins {
id "com.jetbrains.python.envs" version "0.0.26"
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-parcelize'
apply plugin: 'jacoco'
apply plugin: 'androidx.navigation.safeargs.kotlin'
apply plugin: 'com.google.android.gms.oss-licenses-plugin'
import org.gradle.internal.logging.text.StyledTextOutput.Style
import org.gradle.internal.logging.text.StyledTextOutputFactory
import static org.gradle.api.tasks.testing.TestResult.ResultType
apply from: 'benchmark.gradle'
android {
project.maybeConfigForJetpackBenchmark(it)
if (project.hasProperty("testBuildType")) {
// Allowing to configure the test build type via command line flag (./gradlew -PtestBuildType=release ..)
// in order to run UI tests against other build variants than debug in automation.
testBuildType project.property("testBuildType")
}
defaultConfig {
applicationId "com.leos.leosium"
minSdkVersion Config.minSdkVersion
compileSdk Config.compileSdkVersion
targetSdkVersion Config.targetSdkVersion
versionCode 1
versionName Config.generateDebugVersionName(project)
vectorDrawables.useSupportLibrary = true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
resValue "bool", "IS_DEBUG", "false"
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "false"
buildConfigField "String", "GIT_HASH", "\"\"" // see override in release builds for why it's blank.
// This should be the "public" base URL of AMO.
buildConfigField "String", "AMO_BASE_URL", "\"https://addons.mozilla.org\""
buildConfigField "String", "AMO_COLLECTION_NAME", "\"LeOSium-Android\""
buildConfigField "String", "AMO_COLLECTION_USER", "\"17224042\""
// This should be the base URL used to call the AMO API.
buildConfigField "String", "AMO_SERVER_URL", "\"https://services.addons.mozilla.org\""
def deepLinkSchemeValue = "waterfox-dev"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = [
"deepLinkScheme": deepLinkSchemeValue
]
buildConfigField "String[]", "SUPPORTED_LOCALE_ARRAY", getSupportedLocales()
}
def releaseTemplate = {
// We allow disabling optimization by passing `-PdisableOptimization` to gradle. This is used
// in automation for UI testing non-debug builds.
shrinkResources !project.hasProperty("disableOptimization")
minifyEnabled !project.hasProperty("disableOptimization")
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
matchingFallbacks = ['release'] // Use on the "release" build type in dependencies (AARs)
// Changing the build config can cause files that depend on BuildConfig.java to recompile
// so we only set the git hash in release builds to avoid possible recompilation in debug builds.
// TODO: [Waterfox] This times out for some reason. Find a solution.
// buildConfigField "String", "GIT_HASH", "\"${Config.getGitHash()}\""
if (gradle.hasProperty("localProperties.autosignReleaseWithDebugKey")) {
signingConfig signingConfigs.debug
}
if (gradle.hasProperty("localProperties.debuggable")) {
debuggable true
}
}
buildTypes {
debug {
shrinkResources false
minifyEnabled false
applicationIdSuffix ".debug"
resValue "bool", "IS_DEBUG", "true"
pseudoLocalesEnabled true
}
release releaseTemplate >> {
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
applicationIdSuffix ""
def deepLinkSchemeValue = "waterfox"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = [
"deepLinkScheme": deepLinkSchemeValue
]
}
}
buildFeatures {
viewBinding true
buildConfig true
}
androidResources {
// All JavaScript code used internally by GeckoView is packaged in a
// file called omni.ja. If this file is compressed in the APK,
// GeckoView must uncompress it before it can do anything else which
// causes a significant delay on startup.
noCompress 'ja'
// manifest.template.json is converted to manifest.json at build time.
// No need to package the template in the APK.
ignoreAssetsPattern "manifest.template.json"
}
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
unitTests.includeAndroidResources = true
animationsDisabled = true
}
flavorDimensions "engine"
sourceSets {
androidTest {
resources.srcDirs += ['src/androidTest/resources']
}
}
splits {
abi {
enable true
reset()
include "x86", "armeabi-v7a", "arm64-v8a", "x86_64"
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
lint {
lintConfig file("lint.xml")
baseline file("lint-baseline.xml")
}
packagingOptions {
resources {
excludes += ['META-INF/atomicfu.kotlin_module', 'META-INF/AL2.0', 'META-INF/LGPL2.1']
}
jniLibs {
useLegacyPackaging true
}
}
testOptions {
unitTests.returnDefaultValues = true
unitTests.all {
// We keep running into memory issues when running our tests. With this config we
// reserve more memory and also create a new process after every 80 test classes. This
// is a band-aid solution and eventually we should try to find and fix the leaks
// instead. :)
forkEvery = 80
maxHeapSize = "3072m"
minHeapSize = "1024m"
}
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion = Versions.androidx_compose_compiler
}
namespace 'net.waterfox.android'
}
android.applicationVariants.all { variant ->
// -------------------------------------------------------------------------------------------------
// Generate version codes for builds
// -------------------------------------------------------------------------------------------------
def isDebug = variant.buildType.resValues['IS_DEBUG']?.value ?: false
def useReleaseVersioning = variant.buildType.buildConfigFields['USE_RELEASE_VERSIONING']?.value ?: false
println("----------------------------------------------")
println("Variant name: " + variant.name)
println("Application ID: " + [variant.applicationId, variant.buildType.applicationIdSuffix].findAll().join())
println("Build type: " + variant.buildType.name)
println("Flavor: " + variant.flavorName)
if (useReleaseVersioning) {
// The Google Play Store does not allow multiple APKs for the same app that all have the
// same version code. Therefore we need to have different version codes for our ARM and x86
// builds.
def versionName = Config.releaseVersionName(project)
println("versionName override: $versionName")
variant.outputs.each { output ->
def abi = output.getFilter(OutputFile.ABI)
// We use the same version code generator, that we inherited from Fennec, across all channels - even on
// channels that never shipped a Fennec build.
def versionCodeOverride = Config.generateFennecVersionCode(abi)
println("versionCode for $abi = $versionCodeOverride")
output.versionNameOverride = versionName
output.versionCodeOverride = versionCodeOverride
}
} else if (gradle.hasProperty("localProperties.branchBuild.waterfox.version")) {
def versionName = gradle.getProperty("localProperties.branchBuild.waterfox.version")
println("versionName override: $versionName")
variant.outputs.each { output ->
output.versionNameOverride = versionName
}
}
// -------------------------------------------------------------------------------------------------
// BuildConfig: Set variables for Sentry and Crash Reporting
// -------------------------------------------------------------------------------------------------
buildConfigField 'String', 'SENTRY_TOKEN', 'null'
if (!isDebug) {
buildConfigField 'boolean', 'CRASH_REPORTING', 'true'
// Reading sentry token from local file (if it exists). In a release task on Fastlane it will be available.
try {
def token = new File("${rootDir}/.sentry_token").text.trim()
buildConfigField 'String', 'SENTRY_TOKEN', '"' + token + '"'
} catch (FileNotFoundException ignored) {}
} else {
buildConfigField 'boolean', 'CRASH_REPORTING', 'false'
}
def buildDate = Config.generateBuildDate()
// Setting buildDate with every build changes the generated BuildConfig, which slows down the
// build. Only do this for non-debug builds, to speed-up builds produced during local development.
if (isDebug) {
buildConfigField 'String', 'BUILD_DATE', '"debug build"'
} else {
buildConfigField 'String', 'BUILD_DATE', '"' + buildDate + '"'
}
// -------------------------------------------------------------------------------------------------
// MLS: Read token from local file if it exists
// -------------------------------------------------------------------------------------------------
print("MLS token: ")
try {
def token = new File("${rootDir}/.mls_token").text.trim()
buildConfigField 'String', 'MLS_TOKEN', '"' + token + '"'
println "(Added from .mls_token file)"
} catch (FileNotFoundException ignored) {
buildConfigField 'String', 'MLS_TOKEN', '""'
println("X_X")
}
// -------------------------------------------------------------------------------------------------
// BuildConfig: Set flag for official builds; similar to MOZILLA_OFFICIAL in mozilla-central.
// -------------------------------------------------------------------------------------------------
if (project.hasProperty("official") || gradle.hasProperty("localProperties.official")) {
buildConfigField 'Boolean', 'MOZILLA_OFFICIAL', 'true'
} else {
buildConfigField 'Boolean', 'MOZILLA_OFFICIAL', 'false'
}
// -------------------------------------------------------------------------------------------------
// BuildConfig: Set remote wallpaper URL using local file if it exists
// -------------------------------------------------------------------------------------------------
print("Wallpaper URL: ")
try {
def token = new File("${rootDir}/.wallpaper_url").text.trim()
buildConfigField 'String', 'WALLPAPER_URL', '"' + token + '"'
println "(Added from .wallpaper_url file)"
} catch (FileNotFoundException ignored) {
buildConfigField 'String', 'WALLPAPER_URL', '""'
println("--")
}
}
// [Waterfox] try to exclude all telemetry dependencies
configurations.all {
// Telemetry is a transitive dependency of several necessary dependencies,
// thus we have to exclude it globally.
exclude group: 'org.mozilla.telemetry', module: 'glean-native'
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
}
}
dependencies {
implementation Deps.mozilla_browser_engine_gecko
implementation Deps.kotlin_stdlib
implementation Deps.kotlin_coroutines
implementation Deps.kotlin_coroutines_android
testImplementation Deps.kotlin_coroutines_test
implementation Deps.androidx_appcompat
implementation Deps.androidx_activity_compose
implementation Deps.androidx_constraintlayout
implementation Deps.androidx_coordinatorlayout
implementation Deps.google_accompanist_drawablepainter
implementation Deps.google_accompanist_insets
implementation Deps.google_accompanist_swiperefresh
implementation Deps.coil
implementation Deps.sentry
implementation Deps.mozilla_compose_awesomebar
implementation Deps.mozilla_concept_awesomebar
implementation Deps.mozilla_concept_base
implementation Deps.mozilla_concept_engine
implementation Deps.mozilla_concept_menu
implementation Deps.mozilla_concept_push
implementation Deps.mozilla_concept_storage
implementation Deps.mozilla_concept_sync
implementation Deps.mozilla_concept_toolbar
implementation Deps.mozilla_concept_tabstray
implementation Deps.mozilla_browser_domains
implementation Deps.mozilla_browser_icons
implementation Deps.mozilla_browser_menu
implementation Deps.mozilla_browser_menu2
implementation Deps.mozilla_browser_session_storage
implementation Deps.mozilla_browser_state
implementation Deps.mozilla_browser_storage_sync
implementation Deps.mozilla_browser_tabstray
implementation Deps.mozilla_browser_thumbnails
implementation Deps.mozilla_browser_toolbar
implementation Deps.mozilla_feature_addons
implementation Deps.mozilla_feature_accounts
implementation Deps.mozilla_feature_app_links
implementation Deps.mozilla_feature_autofill
implementation Deps.mozilla_feature_awesomebar
implementation Deps.mozilla_feature_contextmenu
implementation Deps.mozilla_feature_customtabs
implementation Deps.mozilla_feature_downloads
implementation Deps.mozilla_feature_intent
implementation Deps.mozilla_feature_media
implementation Deps.mozilla_feature_prompts
implementation Deps.mozilla_feature_push
implementation Deps.mozilla_feature_privatemode
implementation Deps.mozilla_feature_pwa
implementation Deps.mozilla_feature_qr
implementation Deps.mozilla_feature_search
implementation Deps.mozilla_feature_session
implementation Deps.mozilla_feature_syncedtabs
implementation Deps.mozilla_feature_toolbar
implementation Deps.mozilla_feature_tabs
implementation Deps.mozilla_feature_findinpage
implementation Deps.mozilla_feature_logins
implementation Deps.mozilla_feature_site_permissions
implementation Deps.mozilla_feature_readerview
implementation Deps.mozilla_feature_tab_collections
implementation Deps.mozilla_feature_recentlyclosed
implementation Deps.mozilla_feature_top_sites
implementation Deps.mozilla_feature_share
implementation Deps.mozilla_feature_accounts_push
implementation Deps.mozilla_feature_webauthn
implementation Deps.mozilla_feature_webcompat
implementation Deps.mozilla_feature_webnotifications
implementation Deps.mozilla_feature_webcompat_reporter
implementation Deps.mozilla_service_contile
implementation Deps.mozilla_service_digitalassetlinks
implementation Deps.mozilla_service_sync_autofill
implementation Deps.mozilla_service_sync_logins
implementation Deps.mozilla_service_firefox_accounts
implementation Deps.mozilla_service_location
implementation Deps.mozilla_support_extensions
implementation Deps.mozilla_support_base
implementation Deps.mozilla_support_rusterrors
implementation Deps.mozilla_support_images
implementation Deps.mozilla_support_ktx
implementation Deps.mozilla_support_rustlog
implementation Deps.mozilla_support_utils
implementation Deps.mozilla_support_locale
implementation Deps.mozilla_ui_colors
implementation Deps.mozilla_ui_icons
implementation Deps.mozilla_lib_publicsuffixlist
implementation Deps.mozilla_ui_widgets
implementation Deps.mozilla_ui_tabcounter
implementation Deps.mozilla_lib_crash
implementation Deps.lib_crash_sentry
implementation Deps.mozilla_lib_state
implementation Deps.mozilla_lib_dataprotect
debugImplementation Deps.leakcanary
implementation Deps.androidx_compose_ui
implementation Deps.androidx_compose_ui_tooling
implementation Deps.androidx_compose_foundation
implementation Deps.androidx_compose_material
implementation Deps.androidx_compose_paging
implementation Deps.androidx_legacy
implementation Deps.androidx_biometric
implementation Deps.androidx_paging
implementation Deps.androidx_preference
implementation Deps.androidx_fragment
implementation Deps.androidx_navigation_fragment
implementation Deps.androidx_navigation_ui
implementation Deps.androidx_recyclerview
implementation Deps.androidx_lifecycle_common
implementation Deps.androidx_lifecycle_livedata
implementation Deps.androidx_lifecycle_process
implementation Deps.androidx_lifecycle_runtime
implementation Deps.androidx_lifecycle_viewmodel
implementation Deps.androidx_core
implementation Deps.androidx_core_ktx
implementation Deps.androidx_transition
implementation Deps.androidx_work_ktx
implementation Deps.androidx_datastore
implementation Deps.google_material
androidTestImplementation Deps.uiautomator
androidTestImplementation "tools.fastlane:screengrab:2.0.0"
// This Falcon version is added to maven central now required for Screengrab
implementation 'com.jraska:falcon:2.2.0'
androidTestImplementation Deps.androidx_compose_ui_test
androidTestImplementation Deps.espresso_core, {
exclude group: 'com.android.support', module: 'support-annotations'
}
androidTestImplementation(Deps.espresso_contrib) {
exclude module: 'appcompat-v7'
exclude module: 'support-v4'
exclude module: 'support-annotations'
exclude module: 'recyclerview-v7'
exclude module: 'design'
exclude module: 'espresso-core'
exclude module: 'protobuf-lite'
}
androidTestImplementation Deps.androidx_test_core
androidTestImplementation Deps.espresso_idling_resources
androidTestImplementation Deps.espresso_intents
androidTestImplementation Deps.tools_test_runner
androidTestImplementation Deps.tools_test_rules
androidTestUtil Deps.orchestrator
androidTestImplementation Deps.espresso_core, {
exclude group: 'com.android.support', module: 'support-annotations'
}
androidTestImplementation Deps.androidx_junit
androidTestImplementation Deps.androidx_test_extensions
androidTestImplementation Deps.androidx_work_testing
androidTestImplementation Deps.androidx_benchmark_junit4
androidTestImplementation Deps.mockwebserver
testImplementation Deps.mozilla_support_test
testImplementation Deps.mozilla_support_test_libstate
testImplementation Deps.androidx_junit
testImplementation Deps.androidx_test_extensions
testImplementation Deps.androidx_work_testing
testImplementation (Deps.robolectric) {
exclude group: 'org.apache.maven'
}
testImplementation 'org.apache.maven:maven-ant-tasks:2.1.3'
implementation Deps.mozilla_support_rusthttp
testImplementation Deps.mockk
lintChecks project(":mozilla-lint-rules")
}
if (project.hasProperty("coverage")) {
tasks.withType(Test).configureEach {
jacoco.includeNoLocationClasses = true
jacoco.excludes = ['jdk.internal.*']
}
jacoco {
toolVersion = "0.8.7"
}
android.applicationVariants.all { variant ->
tasks.register("jacoco${variant.name.capitalize()}TestReport", JacocoReport) {
dependsOn "test${variant.name.capitalize()}UnitTest"
reports {
xml.enabled = true
html.enabled = true
}
def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*',
'**/*Test*.*', 'android/**/*.*', '**/*$[0-9].*']
def kotlinDebugTree = fileTree(dir: "$project.buildDir/tmp/kotlin-classes/${variant.name}", excludes: fileFilter)
def javaDebugTree = fileTree(dir: "$project.buildDir/intermediates/classes/${variant.flavorName}/${variant.buildType.name}",
excludes: fileFilter)
def mainSrc = "$project.projectDir/src/main/java"
sourceDirectories.setFrom(files([mainSrc]))
classDirectories.setFrom(files([kotlinDebugTree, javaDebugTree]))
executionData.setFrom(fileTree(dir: project.buildDir, includes: [
"jacoco/test${variant.name.capitalize()}UnitTest.exec",
'outputs/code-coverage/connected/*coverage.ec'
]))
}
}
android {
buildTypes {
debug {
testCoverageEnabled true
}
}
}
}
// -------------------------------------------------------------------------------------------------
// Task for printing APK information for the requested variant
// Usage: "./gradlew printVariants
// -------------------------------------------------------------------------------------------------
tasks.register('printVariants') {
doLast {
def variants = android.applicationVariants.collect { variant -> [
apks: variant.outputs.collect { output -> [
abi: output.getFilter(com.android.build.VariantOutput.FilterType.ABI),
fileName: output.outputFile.name
]},
build_type: variant.buildType.name,
name: variant.name,
]}
// AndroidTest is a special case not included above
variants.add([
apks: [[
abi: 'noarch',
fileName: 'app-debug-androidTest.apk',
]],
build_type: 'androidTest',
name: 'androidTest',
])
println 'variants: ' + JsonOutput.toJson(variants)
}
}
afterEvaluate {
// Format test output. Ported from AC #2401
tasks.withType(Test).configureEach {
systemProperty "robolectric.logging", "stdout"
systemProperty "logging.test-mode", "true"
testLogging.events = []
def out = services.get(StyledTextOutputFactory).create("tests")
beforeSuite { descriptor ->
if (descriptor.getClassName() != null) {
out.style(Style.Header).println("\nSUITE: " + descriptor.getClassName())
}
}
beforeTest { descriptor ->
out.style(Style.Description).println(" TEST: " + descriptor.getName())
}
onOutput { descriptor, event ->
logger.lifecycle(" " + event.message.trim())
}
afterTest { descriptor, result ->
switch (result.getResultType()) {
case ResultType.SUCCESS:
out.style(Style.Success).println(" SUCCESS")
break
case ResultType.FAILURE:
out.style(Style.Failure).println(" FAILURE")
logger.lifecycle("", result.getException())
break
case ResultType.SKIPPED:
out.style(Style.Info).println(" SKIPPED")
break
}
logger.lifecycle("")
}
}
}
if (gradle.hasProperty('localProperties.dependencySubstitutions.geckoviewTopsrcdir')) {
if (gradle.hasProperty('localProperties.dependencySubstitutions.geckoviewTopobjdir')) {
ext.topobjdir = gradle."localProperties.dependencySubstitutions.geckoviewTopobjdir"
}
ext.topsrcdir = gradle."localProperties.dependencySubstitutions.geckoviewTopsrcdir"
apply from: "${topsrcdir}/substitute-local-geckoview.gradle"
}
def acSrcDir = null
if (gradle.hasProperty('localProperties.autoPublish.android-components.dir')) {
acSrcDir = gradle.getProperty('localProperties.autoPublish.android-components.dir')
} else if (gradle.hasProperty('localProperties.branchBuild.android-components.dir')) {
acSrcDir = gradle.getProperty('localProperties.branchBuild.android-components.dir')
}
if (acSrcDir) {
if (acSrcDir.startsWith("/")) {
apply from: "${acSrcDir}/substitute-local-ac.gradle"
} else {
apply from: "../${acSrcDir}/substitute-local-ac.gradle"
}
}
def appServicesSrcDir = null
if (gradle.hasProperty('localProperties.autoPublish.application-services.dir')) {
appServicesSrcDir = gradle.getProperty('localProperties.autoPublish.application-services.dir')
} else if (gradle.hasProperty('localProperties.branchBuild.application-services.dir')) {
appServicesSrcDir = gradle.getProperty('localProperties.branchBuild.application-services.dir')
}
if (appServicesSrcDir) {
if (appServicesSrcDir.startsWith("/")) {
apply from: "${appServicesSrcDir}/build-scripts/substitute-local-appservices.gradle"
} else {
apply from: "../${appServicesSrcDir}/build-scripts/substitute-local-appservices.gradle"
}
}
// Define a reusable task for updating the versions of our built-in web extensions. We automate this
// to make sure we never forget to update the version, either in local development or for releases.
// In both cases, we want to make sure the latest version of all extensions (including their latest
// changes) are installed on first start-up.
// We're using the A-C version here as we want to uplift all built-in extensions to A-C (Once that's
// done we can also remove the task below):
// https://github.com/mozilla-mobile/android-components/issues/7249
ext.updateExtensionVersion = { task, extDir ->
configure(task) {
from extDir
include 'manifest.template.json'
rename { 'manifest.json' }
into extDir
def values = ['version': Versions.mozilla_android_components + "." + new Date().format('MMddHHmmss')]
inputs.properties(values)
expand(values)
}
}
android.applicationVariants.configureEach { variant ->
tasks.register("apkSize${variant.name.capitalize()}", ApkSizeTask) {
variantName = variant.name
apks = variant.outputs.collect { output -> output.outputFile.name }
dependsOn "package${variant.name.capitalize()}"
}
}
def getSupportedLocales() {
// This isn't running as a task, instead the array is build when the gradle file is parsed.
// https://github.com/mozilla-mobile/fenix/issues/14175
def foundLocales = new StringBuilder()
foundLocales.append("new String[]{")
fileTree("src/main/res").visit { FileVisitDetails details ->
if (details.file.path.endsWith("${File.separator}strings.xml")) {
def languageCode = details.file.parent.tokenize(File.separator).last().replaceAll('values-', '').replaceAll('-r', '-')
languageCode = (languageCode == "values") ? "en-US" : languageCode
foundLocales.append("\"").append(languageCode).append("\"").append(",")
}
}
foundLocales.append("}")
def foundLocalesString = foundLocales.toString().replaceAll(',}', '}')
return foundLocalesString
}

1462
app/lint-baseline.xml Normal file

File diff suppressed because it is too large Load Diff

94
app/lint.xml Normal file
View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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/. -->
<lint>
<issue id="InvalidPackage">
<!-- The Sentry SDK is compiled against parts of the Java SDK that are not available in the Android SDK.
Let's just ignore issues in the Sentry code since that is a third-party dependency anyways. -->
<ignore path="**/sentry*.jar" />
</issue>
<!-- Lints that don't apply to our translation process -->
<issue id="MissingTranslation" severity="ignore" />
<issue id="PluralsCandidate" severity="ignore" />
<issue id="StringFormatCount" severity="ignore" />
<issue id="TypographyEllipsis" severity="ignore" />
<issue id="ExtraTranslation" severity="ignore" />
<issue id="BrandUsage" severity="error">
<ignore path="**/values-*/strings.xml" />
<ignore path="**/values-*/*_strings.xml" />
<ignore path="**/values/*_strings.xml" />
</issue>
<issue id="IncorrectStraightQuote" severity="error">
<ignore path="**/values-*/strings.xml" />
</issue>
<issue id="IncorrectStraightDoubleQuote" severity="error">
<ignore path="**/values-*/strings.xml" />
<ignore path="**/values/*_strings.xml" />
<ignore path="**/values/arrays.xml" />
</issue>
<issue id="IncorrectEllipsisCharacter" severity="error">
<ignore path="**/values-*/strings.xml" />
</issue>
<issue id="BlankString" severity="error">
<ignore path="**/values-*/strings.xml" />
<ignore path="**/values/*_strings.xml" />
</issue>
<issue id="Typos" severity="error">
<ignore path="**/values-*/strings.xml" />
</issue>
<!-- Lints that are disabled by default -->
<issue id="ConvertToWebp" severity="warning" />
<!-- Performance: we haven't validated that addressing these checks have a significant impact
on performance but they're very quick to fix so we escalate them to error. -->
<!-- Performance: big wins from a theoretical perspective so we escalate to error. -->
<issue id="DrawAllocation" severity="error" />
<issue id="Wakelock" severity="error" />
<issue id="WakelockTimeout" severity="error" />
<issue id="Recycle" severity="error" />
<issue id="StaticFieldLeak" severity="error" />
<issue id="ViewTag" severity="error" />
<issue id="ViewHolder" severity="error" />
<issue id="HandlerLeak" severity="error" />
<issue id="NestedWeights" severity="error" />
<!-- Performance: quick-to-fix violations so we escalate to error.
We haven't validated that they have a significant impact though. -->
<issue id="ObsoleteLayoutParam" severity="error" />
<issue id="ObsoleteSdkInt" severity="error" />
<issue id="AnimatorKeep" severity="error" />
<issue id="DuplicateDivider" severity="error" />
<issue id="MergeRootFrame" severity="error" />
<issue id="UseOfBundledGooglePlayServices" severity="error" />
<issue id="UseValueOf" severity="error" />
<issue id="InefficientWeight" severity="error" />
<issue id="DisableBaselineAlignment" severity="error" />
<issue id="UselessLeaf" severity="error" />
<issue id="UselessParent" severity="error" />
<issue id="UnusedNamespace" severity="error" />
<!-- Performance: checks we'd like to eventually set to error. -->
<issue id="UseCompoundDrawables" severity="warning" />
<issue id="Overdraw" severity="warning" />
<issue id="UnusedResources" severity="error">
<!-- Using an automated process to remove localized strings after they are removed from the default strings.xml
means the files for localized strings will contain unused resources for a few days after the original removal operation. -->
<ignore path="**/values-*/strings.xml" />
</issue>
<!-- Performance: checks that we're unsure of the value of that we might want to investigate. -->
<issue id="UnpackedNativeCode" severity="informational" />
<issue id="LogConditional" severity="informational" />
<issue id="UseSparseArrays" severity="informational" /> <!-- hurts developer convenience of kotlin Map... -->
<issue id="TooDeepLayout" severity="warning" /> <!-- depth can be customized -->
<issue id="TooManyViews" severity="warning" /> <!-- view count can be customized -->
<!-- Ignore due to baseline bloat and lack of actionability -->
<issue id="VectorPath" severity="ignore" />
<!-- Correctness: checks with increased severity -->
<issue id="PrivateResource" severity="error" />
</lint>

122
app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,122 @@
-dontobfuscate
####################################################################################################
# Sentry
####################################################################################################
# Recommended config via https://docs.sentry.io/clients/java/modules/android/#manual-integration
# Since we don't obfuscate, we don't need to use their Gradle plugin to upload ProGuard mappings.
-keepattributes LineNumberTable,SourceFile
-dontwarn org.slf4j.**
-dontwarn javax.**
# Our addition: this class is saved to disk via Serializable, which ProGuard doesn't like.
# If we exclude this, upload silently fails (Sentry swallows a NPE so we don't crash).
# I filed https://github.com/getsentry/sentry-java/issues/572
#
# If Sentry ever mysteriously stops working after we upgrade it, this could be why.
-keep class io.sentry.event.Event { *; }
####################################################################################################
# Android and GeckoView built-ins
####################################################################################################
-dontwarn android.**
-dontwarn androidx.**
-dontwarn com.google.**
-dontwarn org.mozilla.geckoview.**
# Raptor now writes a *-config.yaml file to specify Gecko runtime settings (e.g. the profile dir). This
# file gets deserialized into a DebugConfig object, which is why we need to keep this class
# and its members.
-keep class org.mozilla.gecko.util.DebugConfig { *; }
####################################################################################################
# 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
####################################################################################################
-assumenosideeffects class kotlinx.coroutines.internal.MainDispatcherLoader {
boolean FAST_SERVICE_LOADER_ENABLED return true;
}
####################################################################################################
# Remove debug logs from release builds
####################################################################################################
-assumenosideeffects class android.util.Log {
public static boolean isLoggable(java.lang.String, int);
public static int v(...);
public static int d(...);
}
####################################################################################################
# Mozilla Application Services
####################################################################################################
-keep class mozilla.appservices.** { *; }
####################################################################################################
# ViewModels
####################################################################################################
-keep class net.waterfox.android.**ViewModel { *; }
-keep class com.google.android.gms.common.ConnectionResult {
int SUCCESS;
}
-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient {
com.google.android.gms.ads.identifier.AdvertisingIdClient$Info getAdvertisingIdInfo(android.content.Context);
}
-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient$Info {
java.lang.String getId();
boolean isLimitAdTrackingEnabled();
}
-keep public class com.android.installreferrer.** { *; }
-keep class dalvik.system.VMRuntime {
java.lang.String getRuntime();
}
-keep class android.os.Build {
java.lang.String[] SUPPORTED_ABIS;
java.lang.String CPU_ABI;
}
-keep class android.content.res.Configuration {
android.os.LocaledList getLocales();
java.util.Locale locale;
}
-keep class android.os.LocaleList {
java.util.Locale get(int);
}
# Keep motionlayout internal methods
# https://github.com/mozilla-mobile/fenix/issues/2094
-keep class androidx.constraintlayout.** { *; }
-keep class com.google.android.gms.common.ConnectionResult {
int SUCCESS;
}
-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient {
com.google.android.gms.ads.identifier.AdvertisingIdClient$Info getAdvertisingIdInfo(android.content.Context);
}
-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient$Info {
java.lang.String getId();
boolean isLimitAdTrackingEnabled();
}
-keep public class com.android.installreferrer.** { *; }
# Keep Android Lifecycle methods
# https://bugzilla.mozilla.org/show_bug.cgi?id=1596302
-keep class androidx.lifecycle.** { *; }
-dontwarn java.beans.BeanInfo
-dontwarn java.beans.FeatureDescriptor
-dontwarn java.beans.IntrospectionException
-dontwarn java.beans.Introspector
-dontwarn java.beans.PropertyDescriptor

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,87 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.leos.leosium",
"variantName": "release",
"elements": [
{
"type": "ONE_OF_MANY",
"filters": [
{
"filterType": "ABI",
"value": "armeabi-v7a"
}
],
"attributes": [],
"versionCode": 2024497681,
"versionName": "1.0.9.2",
"outputFile": "app-armeabi-v7a-release.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
{
"filterType": "ABI",
"value": "x86_64"
}
],
"attributes": [],
"versionCode": 2024497681,
"versionName": "1.0.9.2",
"outputFile": "app-x86_64-release.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
{
"filterType": "ABI",
"value": "arm64-v8a"
}
],
"attributes": [],
"versionCode": 2024497681,
"versionName": "1.0.9.2",
"outputFile": "app-arm64-v8a-release.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
{
"filterType": "ABI",
"value": "x86"
}
],
"attributes": [],
"versionCode": 2024497681,
"versionName": "1.0.9.2",
"outputFile": "app-x86-release.apk"
}
],
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-armeabi-v7a-release.dm",
"baselineProfiles/1/app-x86_64-release.dm",
"baselineProfiles/1/app-arm64-v8a-release.dm",
"baselineProfiles/1/app-x86-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-armeabi-v7a-release.dm",
"baselineProfiles/0/app-x86_64-release.dm",
"baselineProfiles/0/app-arm64-v8a-release.dm",
"baselineProfiles/0/app-x86-release.dm"
]
}
],
"minSdkVersionForDexing": 21
}

View File

@ -0,0 +1,17 @@
<html>
<head>
<meta name="viewport" content="width=device-width">
<title>Address_Form</title>
</head>
<body>
<form>
<p>Street Address: <input id="streetAddress" type="text"></p>
<p>City: <input id="city" type="text"></p>
<p>Zip Code: <input id="zipCode" type="text"></p>
<p>Country: <input id="country" type="text"></p>
<p>Telephone: <input id="telephone" type="text"></p>
<p>Email: <input id="email" type="text"></p>
</form>
</body>
</html>

View File

@ -0,0 +1,13 @@
<html>
<head>
<title>Audio_Test_Page</title>
</head>
<body>
<p id="testContent">Page content: audio player</p>
<div class="audioPlayer">
<audio id="audioSample" controls loop>
<source src="../resources/audioSample.mp3">
</audio>
</div>
</body>
</html>

View File

@ -0,0 +1,13 @@
<html>
<head>
<meta name="viewport" content="width=device-width">
<title>Credit_Card_Form</title>
</head>
<body>
<form>
<p>Card information</p>
<p>Card Number: <input id="cardNumber" type="text" placeholder="1234 1234 1234 1234"></p>
<p>Name on card: <input id="nameOnCard"type="text" placeholder="Name on card"></p>
</form>
</body>
</html>

View File

@ -0,0 +1,10 @@
<html>
<head>
<title>Test_Page_1</title>
</head>
<body>
<h1>
<p id="testContent">Page content: 1</p>
</h1>
</body>
</html>

View File

@ -0,0 +1,10 @@
<html>
<head>
<title>Test_Page_2</title>
</head>
<body>
<h1>
<p id="testContent">Page content: 2</p>
</h1>
</body>
</html>

View File

@ -0,0 +1,13 @@
<html>
<head>
<title>Test_Page_3</title>
</head>
<body>
<h1>
<p id="testContent">Page content: 3</p>
<p>
<a href="https://play.google.com/store/apps/details?id=net.waterfox.android">Mozilla Playstore link</a>
</p>
</h1>
</body>
</html>

View File

@ -0,0 +1,20 @@
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Test_Page_4</title>
</head>
<body>
<p id="testContent">Page content: 4</p>
<a href="generic1.html">Link 1</a>
<a href="generic2.html">Link 2</a>
<a href="generic3.html">Link 3</a>
<p>
<a href="../resources/rabbit.jpg">
<img src="../resources/rabbit.jpg" alt="test_link_image">
</a>
</p>
<p>
<img src="../resources/rabbit.jpg" alt="test_no_link_image">
</p>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy
eirmod tempor invidunt</title>
<meta content="width=device-width, initial-scale=1"
name="viewport"/>
</head>
<body>
<p id="testContent">Page content: lorem ipsum</p>
<h1>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt</h1>
<p>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy
eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit
amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor
sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed
diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor
sit amet.
</p>
</body>
</html>

View File

@ -0,0 +1,22 @@
<html>
<head>
<meta name="viewport" content="width=device-width">
</head>
<body aria-label="body">
<form method="GET" action="passwordsubmit.html">
<p>Username: <input id="username" type="text" value="test@example.com"></p>
<p>Password: <input id="password" type="password" value="verysecret"></p>
<p><input type="submit" id="submit" value="Login" aria-label="submit"/></p>
</form>
</body>
<script>
document.getElementById("password").value = Math.random().toString();
</script>
</html>

View File

@ -0,0 +1,9 @@
<html>
<head>
<meta name="viewport" content="width=device-width">
</head>
<body aria-label="body">
<p>Password submitted. Nope just a test.</p>
</body>
</html>

View File

@ -0,0 +1,45 @@
<html>
<script src="jquery-3.4.1.slim.min.js"></script>
<script>
function setCookie(newVal){
window.document.cookie = "pageStatus = " + newVal + ";";
}
function readCookie(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for(var i=0;i < ca.length;i++) {
var c = ca[i];
while (c.charAt(0)==' ') c = c.substring(1,c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
}
return null;
}
function valSwap(){
currentCookie = readCookie("pageStatus");
if(currentCookie == null) {
setCookie("DEFAULT");
}
if (currentCookie.localeCompare("REFRESHED") == 0) {
setCookie("DEFAULT");
return "DEFAULT";
} else {
setCookie("REFRESHED");
return "REFRESHED";
}
}
var textToShow = valSwap();
window.addEventListener('DOMContentLoaded', (event) => {
document.querySelector('h1').innerHTML = textToShow;
});
</script>
<body>
<h1>DEFAULT</h1>
</body>
</html>

View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<meta name="viewport" content="width=device-width">
<body>
<h1>Storage check</h1>
<script type="text/javascript">
if (sessionStorage.getItem('sessionTest') == 'session storage') {
document.write('<p>Session storage has value</p>');
} else {
document.write('<p>Session storage empty</p>');
}
if (localStorage.getItem('localTest') == 'local storage') {
document.write('<p>Local storage has value</p>');
} else {
document.write('<p>Local storage empty</p>');
}
</script>
</body>
</html>

View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<meta name="viewport" content="width=device-width">
<body>
<h1>Storage Write</h1>
<p id="cookies"></p>
<button id="setCookies">Set cookies</button>
<script type="text/javascript">
(function() {
document.getElementById("cookies").textContent = document.cookie?document.cookie:"No cookies set";
})();
document.getElementById("setCookies").addEventListener("click", function() {
document.cookie = "user=android";
document.getElementById("cookies").textContent = document.cookie;
});
sessionStorage.setItem('sessionTest', 'session storage');
localStorage.setItem('localTest', 'local storage');
document.write('<p>Values written to storage</p>');
</script>
</body>
</html>

View File

@ -0,0 +1,79 @@
<!DOCTYPE HTML>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<html dir="ltr" xml:lang="en-US" lang="en-US">
<head>
<meta charset="utf8">
<script src="../resources/trackingAPI.js" type="text/javascript"></script>
</head>
<body>
<iframe src="http://trackertest.org/"></iframe>
<h3>Level 1 (Basic) List</h3>
<p>social-track-digest256:</p>
<img
src="https://social-track-digest256.dummytracker.org/test_not_blocked.png"
onerror="this.onerror=null;this.src='https://not-a-tracker.dummytracker.org/test_blocked.png'">
<br/>
<p>ads-track-digest256:</p>
<img
src="https://ads-track-digest256.dummytracker.org/test_not_blocked.png"
onerror="this.onerror=null;this.src='https://not-a-tracker.dummytracker.org/test_blocked.png'">
<br/>
<p>analytics-track-digest256:</p>
<img
src="https://analytics-track-digest256.dummytracker.org/test_not_blocked.png"
onerror="this.onerror=null;this.src='https://not-a-tracker.dummytracker.org/test_blocked.png'">
<br/>
<p>Fingerprinting:
<pre id="result">test not run</pre>
<script src="https://base-fingerprinting-track-digest256.dummytracker.org/tracker.js"
onerror="this.onerror=null;var result=document.getElementById('result');result.innerHTML='blocked';"
onload="this.onload=null;var result=document.getElementById('result');result.innerHTML='NOT blocked';"
></script>
</p>
<br/>
<p>Cryptomining:
<img
src="https://base-cryptomining-track-digest256.dummytracker.org/test_not_blocked.png" alt="not blocked"
onerror="this.onerror=null;this.src='https://not-a-tracker.dummytracker.org/test_blocked.png';this.alt='blocked'">
</p>
<p><b>Cookie blocking</b>
</p>
<iframe height=0 width=0 src="https://social-tracking-protection-facebook-digest256.dummytracker.org/cookie_access_test.html?test_origin=senglehardt.com"></iframe>
<iframe height=0 width=0 src="https://social-tracking-protection-linkedin-digest256.dummytracker.org/cookie_access_test.html?test_origin=senglehardt.com"></iframe>
<iframe height=0 width=0 src="https://social-tracking-protection-twitter-digest256.dummytracker.org/cookie_access_test.html?test_origin=senglehardt.com"></iframe>
<p>
* Facebook-cookies <pre id="social-tracking-protection-facebook-digest256"></pre>
* LinkedIn-cookies <pre id="social-tracking-protection-linkedin-digest256"></pre>
* Twitter-cookies <pre id="social-tracking-protection-twitter-digest256"></pre>
</p>
<script>
function updateCookieStatus(statusMessage, list) {
var output = document.getElementById(list);
if (statusMessage === 'cookies') {
output.innerHTML = "Cookies not blocked";
} else if (statusMessage === 'no_cookies') {
output.innerHTML = "Blocked";
} else {
output.innerHTML = "Unrecognized status";
}
}
window.addEventListener("message", event => {
lists = [
'social-tracking-protection-facebook-digest256',
'social-tracking-protection-linkedin-digest256',
'social-tracking-protection-twitter-digest256'
];
lists.forEach(list => {
if (event.origin === `https://${list}.dummytracker.org`) {
updateCookieStatus(event.data, list);
}
});
}, false);
</script>
</body>
</html>

View File

@ -0,0 +1,14 @@
<html>
<head>
<title>Video_Test_Page</title>
<meta name="viewport" content="width=device-width">
</head>
<body>
<p id="testContent">Page content: video player</p>
<div id="video-container">
<video id="videoSample" width="320" height="240" controls loop>
<source src="../resources/clip.mp4">
</video>
</div>
</body>
</html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,70 @@
function createIframe(src) {
let ifr = document.createElement("iframe");
ifr.src = src;
document.body.appendChild(ifr);
}
function createImage(src) {
let img = document.createElement("img");
img.src = src;
img.onload = () => {
parent.postMessage("done", "*");
};
document.body.appendChild(img);
}
onmessage = event => {
switch (event.data) {
case "tracking":
createIframe("https://trackertest.org/");
break;
case "socialtracking":
createIframe(
"https://social-tracking.example.org/browser/browser/base/content/test/protectionsUI/cookieServer.sjs"
);
break;
case "cryptomining":
createIframe("http://cryptomining.example.com/");
break;
case "fingerprinting":
createIframe("https://fingerprinting.example.com/");
break;
case "more-tracking":
createIframe("https://itisatracker.org/");
break;
case "cookie":
createIframe(
"https://trackertest.org/browser/browser/base/content/test/protectionsUI/cookieServer.sjs"
);
break;
case "first-party-cookie":
// Since the content blocking log doesn't seem to get updated for
// top-level cookies right now, we just create an iframe with the
// first party domain...
createIframe(
"http://not-tracking.example.com/browser/browser/base/content/test/protectionsUI/cookieServer.sjs"
);
break;
case "third-party-cookie":
createIframe(
"https://test1.example.org/browser/browser/base/content/test/protectionsUI/cookieServer.sjs"
);
break;
case "image":
createImage(
"http://trackertest.org/browser/browser/base/content/test/protectionsUI/cookieServer.sjs?type=image-no-cookie"
);
break;
case "window-open":
window.win = window.open(
"http://trackertest.org/browser/browser/base/content/test/protectionsUI/cookieServer.sjs",
"_blank",
"width=100,height=100"
);
break;
case "window-close":
window.win.close();
window.win = null;
break;
}
};

View File

@ -0,0 +1,213 @@
/* 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 net.waterfox.android
import android.content.Context
import android.net.ConnectivityManager
import androidx.core.content.getSystemService
import androidx.navigation.NavController
import mozilla.components.browser.errorpages.ErrorPages
import mozilla.components.browser.errorpages.ErrorType
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.request.RequestInterceptor
import net.waterfox.android.ext.components
import net.waterfox.android.ext.isOnline
import net.waterfox.android.helpers.TestHelper.appContext
import java.lang.ref.WeakReference
/**
* This class overrides the application's request interceptor to
* deactivate the FxA web channel
* which is not supported on the staging servers.
*/
class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
private var navController: WeakReference<NavController>? = null
fun setNavigationController(navController: NavController) {
this.navController = WeakReference(navController)
}
override fun onLoadRequest(
engineSession: EngineSession,
uri: String,
lastUri: String?,
hasUserGesture: Boolean,
isSameDomain: Boolean,
isRedirect: Boolean,
isDirectNavigation: Boolean,
isSubframeRequest: Boolean
): RequestInterceptor.InterceptionResponse? {
interceptFxaRequest(
engineSession,
uri,
lastUri,
hasUserGesture,
isSameDomain,
isRedirect,
isDirectNavigation,
isSubframeRequest
)?.let { response ->
return response
}
interceptAmoRequest(uri, isSameDomain, hasUserGesture)?.let { response ->
return response
}
return context.components.services.appLinksInterceptor
.onLoadRequest(
engineSession,
uri,
lastUri,
hasUserGesture,
isSameDomain,
isRedirect,
isDirectNavigation,
isSubframeRequest
)
}
override fun onErrorRequest(
session: EngineSession,
errorType: ErrorType,
uri: String?
): RequestInterceptor.ErrorResponse? {
val improvedErrorType = improveErrorType(errorType)
val riskLevel = getRiskLevel(improvedErrorType)
val errorPageUri = ErrorPages.createUrlEncodedErrorPage(
context = context,
errorType = improvedErrorType,
uri = uri,
htmlResource = riskLevel.htmlRes
)
return RequestInterceptor.ErrorResponse(errorPageUri)
}
/**
* Checks if the provided [uri] is a request to install an add-on from addons.mozilla.org and
* redirects to Add-ons Manager to trigger installation if needed.
*
* @return [RequestInterceptor.InterceptionResponse.Deny] when installation was triggered and
* the original request can be skipped, otherwise null to continue loading the page.
*/
private fun interceptAmoRequest(
uri: String,
isSameDomain: Boolean,
hasUserGesture: Boolean
): RequestInterceptor.InterceptionResponse? {
// First we execute a quick check to see if this is a request we're interested in i.e. a
// request triggered by the user and coming from AMO.
if (hasUserGesture && isSameDomain && uri.startsWith(AMO_BASE_URL)) {
// Check if this is a request to install an add-on.
val matchResult = AMO_INSTALL_URL_REGEX.toRegex().matchEntire(uri)
if (matchResult != null) {
// Navigate and trigger add-on installation.
matchResult.groupValues.getOrNull(1)?.let { addonId ->
navController?.get()?.navigate(
NavGraphDirections.actionGlobalAddonsManagementFragment(addonId)
)
// We've redirected to the add-ons management fragment, skip original request.
return RequestInterceptor.InterceptionResponse.Deny
}
}
}
// In all other case we let the original request proceed.
return null
}
@Suppress("LongParameterList")
private fun interceptFxaRequest(
engineSession: EngineSession,
uri: String,
lastUri: String?,
hasUserGesture: Boolean,
isSameDomain: Boolean,
isRedirect: Boolean,
isDirectNavigation: Boolean,
isSubframeRequest: Boolean
): RequestInterceptor.InterceptionResponse? {
return appContext.components.services.accountsAuthFeature.interceptor.onLoadRequest(
engineSession,
uri,
lastUri,
hasUserGesture,
isSameDomain,
isRedirect,
isDirectNavigation,
isSubframeRequest
)
}
/**
* Where possible, this will make the error type more accurate by including information not
* available to AC.
*/
private fun improveErrorType(errorType: ErrorType): ErrorType {
// This is not an ideal solution. For context, see:
// https://github.com/mozilla-mobile/android-components/pull/5068#issuecomment-558415367
val isConnected: Boolean = context.getSystemService<ConnectivityManager>()!!.isOnline()
return when {
errorType == ErrorType.ERROR_UNKNOWN_HOST && !isConnected -> ErrorType.ERROR_NO_INTERNET
else -> errorType
}
}
private fun getRiskLevel(errorType: ErrorType): RiskLevel = when (errorType) {
ErrorType.UNKNOWN,
ErrorType.ERROR_NET_INTERRUPT,
ErrorType.ERROR_NET_TIMEOUT,
ErrorType.ERROR_CONNECTION_REFUSED,
ErrorType.ERROR_UNKNOWN_SOCKET_TYPE,
ErrorType.ERROR_REDIRECT_LOOP,
ErrorType.ERROR_OFFLINE,
ErrorType.ERROR_NET_RESET,
ErrorType.ERROR_UNSAFE_CONTENT_TYPE,
ErrorType.ERROR_CORRUPTED_CONTENT,
ErrorType.ERROR_CONTENT_CRASHED,
ErrorType.ERROR_INVALID_CONTENT_ENCODING,
ErrorType.ERROR_UNKNOWN_HOST,
ErrorType.ERROR_MALFORMED_URI,
ErrorType.ERROR_FILE_NOT_FOUND,
ErrorType.ERROR_FILE_ACCESS_DENIED,
ErrorType.ERROR_PROXY_CONNECTION_REFUSED,
ErrorType.ERROR_UNKNOWN_PROXY_HOST,
ErrorType.ERROR_NO_INTERNET,
ErrorType.ERROR_HTTPS_ONLY,
ErrorType.ERROR_BAD_HSTS_CERT,
ErrorType.ERROR_UNKNOWN_PROTOCOL -> RiskLevel.Low
ErrorType.ERROR_SECURITY_BAD_CERT,
ErrorType.ERROR_SECURITY_SSL,
ErrorType.ERROR_PORT_BLOCKED -> RiskLevel.Medium
ErrorType.ERROR_SAFEBROWSING_HARMFUL_URI,
ErrorType.ERROR_SAFEBROWSING_MALWARE_URI,
ErrorType.ERROR_SAFEBROWSING_PHISHING_URI,
ErrorType.ERROR_SAFEBROWSING_UNWANTED_URI -> RiskLevel.High
}
internal enum class RiskLevel(val htmlRes: String) {
Low(LOW_AND_MEDIUM_RISK_ERROR_PAGES),
Medium(LOW_AND_MEDIUM_RISK_ERROR_PAGES),
High(HIGH_RISK_ERROR_PAGES),
}
companion object {
internal const val LOW_AND_MEDIUM_RISK_ERROR_PAGES = "low_and_medium_risk_error_pages.html"
internal const val HIGH_RISK_ERROR_PAGES = "high_risk_error_pages.html"
internal const val AMO_BASE_URL = BuildConfig.AMO_BASE_URL
internal const val AMO_INSTALL_URL_REGEX = "$AMO_BASE_URL/android/downloads/file/([^\\s]+)/([^\\s]+\\.xpi)"
}
}

View File

@ -0,0 +1,23 @@
/* 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 net.waterfox.android.components
import android.content.Context
import mozilla.appservices.fxaclient.FxaServer
import mozilla.components.service.fxa.ServerConfig
/**
* Utility to configure Waterfox Account stage servers.
*/
object FxaServer {
private const val CLIENT_ID = "a2270f727f45f648"
private const val REDIRECT_URL = "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel"
@Suppress("UNUSED_PARAMETER")
fun config(context: Context): ServerConfig {
return ServerConfig(FxaServer.Stage, CLIENT_ID, REDIRECT_URL)
}
}

View File

@ -0,0 +1,13 @@
/* 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 net.waterfox.android.customannotations
/**
* A custom annotation to mark the smoke tests corresponding to the ones in TestRail:
* https://testrail.stage.mozaws.net/index.php?/suites/view/3192
*/
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class SmokeTest

View File

@ -0,0 +1,37 @@
/* 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 net.waterfox.android.helpers
import android.graphics.Bitmap
/* 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.graphics.Color
import org.junit.Assert.assertEquals
/**
* Asserts the two bitmaps are the same by ensuring their dimensions, config, and
* pixel data are the same (within the provided delta): this is the same metrics that
* [Bitmap.sameAs] uses.
*/
fun assertEqualsWithDelta(expectedB: Bitmap, actualB: Bitmap, delta: Float) {
assertEquals("widths should be equal", expectedB.width, actualB.width)
assertEquals("heights should be equal", expectedB.height, actualB.height)
assertEquals("config should be equal", expectedB.config, actualB.config)
for (i in 0 until expectedB.width) {
for (j in 0 until expectedB.height) {
val ePx = expectedB.getPixel(i, j)
val aPx = actualB.getPixel(i, j)
val warn = "Pixel ${i}x$j"
assertEquals("$warn a", Color.alpha(ePx).toFloat(), Color.alpha(aPx).toFloat(), delta)
assertEquals("$warn r", Color.red(ePx).toFloat(), Color.red(aPx).toFloat(), delta)
assertEquals("$warn g", Color.green(ePx).toFloat(), Color.green(aPx).toFloat(), delta)
assertEquals("$warn b", Color.blue(ePx).toFloat(), Color.blue(aPx).toFloat(), delta)
}
}
}

View File

@ -0,0 +1,24 @@
/* 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 net.waterfox.android.helpers
object Constants {
// Device or AVD requires a Google Services Android OS installation
object PackageName {
const val GOOGLE_PLAY_SERVICES = "com.android.vending"
const val GOOGLE_APPS_PHOTOS = "com.google.android.apps.photos"
const val GOOGLE_QUICK_SEARCH = "com.google.android.googlequicksearchbox"
const val YOUTUBE_APP = "com.google.android.youtube"
const val GMAIL_APP = "com.google.android.gm"
const val PHONE_APP = "com.android.dialer"
const val ANDROID_SETTINGS = "com.android.settings"
}
const val SPEECH_RECOGNITION = "android.speech.action.RECOGNIZE_SPEECH"
const val LONG_CLICK_DURATION: Long = 5000
const val LISTS_MAXSWIPES: Int = 3
const val RETRY_COUNT = 3
}

View File

@ -0,0 +1,63 @@
/* 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 net.waterfox.android.helpers
import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import net.waterfox.android.ext.settings
class FeatureSettingsHelper {
private val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
private val settings = context.settings()
// saving default values of feature flags
private var isJumpBackInCFREnabled: Boolean = settings.shouldShowJumpBackInCFR
private var isRecentTabsFeatureEnabled: Boolean = settings.showRecentTabsFeature
private var isRecentlyVisitedFeatureEnabled: Boolean = settings.historyMetadataUIFeature
private var isUserKnowsAboutPwasTrue: Boolean = settings.userKnowsAboutPwas
private var isTCPCFREnabled: Boolean = settings.shouldShowTotalCookieProtectionCFR
fun setJumpBackCFREnabled(enabled: Boolean) {
settings.shouldShowJumpBackInCFR = enabled
}
fun setRecentTabsFeatureEnabled(enabled: Boolean) {
settings.showRecentTabsFeature = enabled
}
fun setRecentlyVisitedFeatureEnabled(enabled: Boolean) {
settings.historyMetadataUIFeature = enabled
}
fun setStrictETPEnabled() {
settings.setStrictETP()
}
fun disablePwaCFR(disable: Boolean) {
settings.userKnowsAboutPwas = disable
}
fun deleteSitePermissions(delete: Boolean) {
settings.deleteSitePermissions = delete
}
/**
* Enable or disable showing the TCP CFR when accessing a webpage for the first time.
*/
fun setTCPCFREnabled(shouldShowCFR: Boolean) {
settings.shouldShowTotalCookieProtectionCFR = shouldShowCFR
}
// Important:
// Use this after each test if you have modified these feature settings
// to make sure the app goes back to the default state
fun resetAllFeatureFlags() {
settings.shouldShowJumpBackInCFR = isJumpBackInCFREnabled
settings.showRecentTabsFeature = isRecentTabsFeatureEnabled
settings.historyMetadataUIFeature = isRecentlyVisitedFeatureEnabled
settings.userKnowsAboutPwas = isUserKnowsAboutPwasTrue
settings.shouldShowTotalCookieProtectionCFR = isTCPCFREnabled
}
}

View File

@ -0,0 +1,102 @@
/* 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/. */
@file:Suppress("DEPRECATION")
package net.waterfox.android.helpers
import android.view.ViewConfiguration.getLongPressTimeout
import androidx.test.espresso.intent.rule.IntentsTestRule
import androidx.test.rule.ActivityTestRule
import androidx.test.uiautomator.UiSelector
import net.waterfox.android.HomeActivity
import net.waterfox.android.helpers.TestHelper.appContext
import net.waterfox.android.helpers.TestHelper.mDevice
import net.waterfox.android.onboarding.WaterfoxOnboarding
/**
* A [org.junit.Rule] to handle shared test set up for tests on [HomeActivity].
*
* @param initialTouchMode See [ActivityTestRule]
* @param launchActivity See [ActivityTestRule]
*/
class HomeActivityTestRule(
initialTouchMode: Boolean = false,
launchActivity: Boolean = true,
private val skipOnboarding: Boolean = false
) :
ActivityTestRule<HomeActivity>(HomeActivity::class.java, initialTouchMode, launchActivity) {
private val longTapUserPreference = getLongPressTimeout()
override fun beforeActivityLaunched() {
super.beforeActivityLaunched()
setLongTapTimeout(3000)
if (skipOnboarding) { skipOnboardingBeforeLaunch() }
}
override fun afterActivityFinished() {
super.afterActivityFinished()
setLongTapTimeout(longTapUserPreference)
closeNotificationShade()
}
}
/**
* A [org.junit.Rule] to handle shared test set up for tests on [HomeActivity]. This adds
* functionality for using the Espresso-intents api, and extends from ActivityTestRule.
*
* @param initialTouchMode See [IntentsTestRule]
* @param launchActivity See [IntentsTestRule]
*/
class HomeActivityIntentTestRule(
initialTouchMode: Boolean = false,
launchActivity: Boolean = true,
private val skipOnboarding: Boolean = false
) :
IntentsTestRule<HomeActivity>(HomeActivity::class.java, initialTouchMode, launchActivity) {
private val longTapUserPreference = getLongPressTimeout()
override fun beforeActivityLaunched() {
super.beforeActivityLaunched()
setLongTapTimeout(3000)
if (skipOnboarding) { skipOnboardingBeforeLaunch() }
}
override fun afterActivityFinished() {
super.afterActivityFinished()
setLongTapTimeout(longTapUserPreference)
closeNotificationShade()
}
}
// changing the device preference for Touch and Hold delay, to avoid long-clicks instead of a single-click
fun setLongTapTimeout(delay: Int) {
// Issue: https://github.com/mozilla-mobile/fenix/issues/25132
var attempts = 0
while (attempts++ < 3) {
try {
mDevice.executeShellCommand("settings put secure long_press_timeout $delay")
break
} catch (e: RuntimeException) {
e.printStackTrace()
}
}
}
private fun skipOnboardingBeforeLaunch() {
// The production code isn't aware that we're using
// this API so it can be fragile.
WaterfoxOnboarding(appContext).finish()
}
private fun closeNotificationShade() {
if (mDevice.findObject(
UiSelector().resourceId("com.android.systemui:id/notification_stack_scroller")
).exists()
) {
mDevice.pressHome()
}
}

View File

@ -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 http://mozilla.org/MPL/2.0/. */
@file:Suppress("DEPRECATION")
package net.waterfox.android.helpers
import androidx.test.espresso.IdlingRegistry
import androidx.test.rule.ActivityTestRule
import net.waterfox.android.HomeActivity
import net.waterfox.android.helpers.idlingresource.AddonsInstallingIdlingResource
object IdlingResourceHelper {
// Idling Resource to manage installing an addon
fun registerAddonInstallingIdlingResource(activityTestRule: ActivityTestRule<HomeActivity>) {
IdlingRegistry.getInstance().register(
AddonsInstallingIdlingResource(
activityTestRule.activity.supportFragmentManager
)
)
}
fun unregisterAddonInstallingIdlingResource(activityTestRule: ActivityTestRule<HomeActivity>) {
IdlingRegistry.getInstance().unregister(
AddonsInstallingIdlingResource(
activityTestRule.activity.supportFragmentManager
)
)
}
fun unregisterAllIdlingResources() {
for (resource in IdlingRegistry.getInstance().resources) {
IdlingRegistry.getInstance().unregister(resource)
}
}
}

View File

@ -0,0 +1,92 @@
/* 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 net.waterfox.android.helpers
import androidx.test.uiautomator.UiObject
import androidx.test.uiautomator.UiSelector
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import net.waterfox.android.helpers.TestAssetHelper.waitingTime
import net.waterfox.android.helpers.TestHelper.mDevice
/**
* Helper for querying and interacting with items based on their matchers.
*/
object MatcherHelper {
fun itemWithResId(resourceId: String) =
mDevice.findObject(UiSelector().resourceId(resourceId))
fun itemContainingText(itemText: String) =
mDevice.findObject(UiSelector().textContains(itemText))
fun itemWithDescription(description: String) =
mDevice.findObject(UiSelector().descriptionContains(description))
fun checkedItemWithResId(resourceId: String, isChecked: Boolean) =
mDevice.findObject(UiSelector().resourceId(resourceId).checked(isChecked))
fun checkedItemWithResIdAndText(resourceId: String, text: String, isChecked: Boolean) =
mDevice.findObject(
UiSelector()
.resourceId(resourceId)
.textContains(text)
.checked(isChecked),
)
fun itemWithResIdAndDescription(resourceId: String, description: String) =
mDevice.findObject(UiSelector().resourceId(resourceId).descriptionContains(description))
fun itemWithResIdAndText(resourceId: String, text: String) =
mDevice.findObject(UiSelector().resourceId(resourceId).text(text))
fun assertItemWithResIdExists(vararg appItems: UiObject, exists: Boolean = true) {
if (exists) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
} else {
for (appItem in appItems) {
assertFalse(appItem.waitForExists(waitingTime))
}
}
}
fun assertItemContainingTextExists(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
}
fun assertItemWithDescriptionExists(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
}
fun assertCheckedItemWithResIdExists(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
}
fun assertCheckedItemWithResIdAndTextExists(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
}
fun assertItemWithResIdAndDescriptionExists(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
}
fun assertItemWithResIdAndTextExists(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
}
}

View File

@ -0,0 +1,72 @@
/* 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 net.waterfox.android.helpers
import android.graphics.Bitmap
import android.view.View
import android.view.ViewGroup
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import junit.framework.AssertionFailedError
import org.hamcrest.CoreMatchers.not
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.TypeSafeMatcher
import net.waterfox.android.helpers.matchers.BitmapDrawableMatcher
import androidx.test.espresso.matcher.ViewMatchers.isChecked as espressoIsChecked
import androidx.test.espresso.matcher.ViewMatchers.isEnabled as espressoIsEnabled
import androidx.test.espresso.matcher.ViewMatchers.isSelected as espressoIsSelected
/**
* The [espressoIsEnabled] function that can also handle disabled state through the boolean argument.
*/
fun isEnabled(isEnabled: Boolean): Matcher<View> = maybeInvertMatcher(espressoIsEnabled(), isEnabled)
/**
* The [espressoIsChecked] function that can also handle unchecked state through the boolean argument.
*/
fun isChecked(isChecked: Boolean): Matcher<View> = maybeInvertMatcher(espressoIsChecked(), isChecked)
/**
* The [espressoIsSelected] function that can also handle not selected state through the boolean argument.
*/
fun isSelected(isSelected: Boolean): Matcher<View> = maybeInvertMatcher(espressoIsSelected(), isSelected)
private fun maybeInvertMatcher(matcher: Matcher<View>, useUnmodifiedMatcher: Boolean): Matcher<View> = when {
useUnmodifiedMatcher -> matcher
else -> not(matcher)
}
fun withBitmapDrawable(bitmap: Bitmap, name: String): Matcher<View>? = BitmapDrawableMatcher(bitmap, name)
fun nthChildOf(
parentMatcher: Matcher<View>,
childPosition: Int
): Matcher<View> {
return object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) {
description.appendText("Position is $childPosition")
}
public override fun matchesSafely(view: View): Boolean {
if (view.parent !is ViewGroup) {
return parentMatcher.matches(view.parent)
}
val group = view.parent as ViewGroup
return parentMatcher.matches(view.parent) && group.getChildAt(childPosition) == view
}
}
}
fun ViewInteraction.isVisibleForUser(): Boolean {
try {
check(matches(ViewMatchers.isCompletelyDisplayed()))
} catch (assertionError: AssertionFailedError) {
return false
}
return true
}

View File

@ -0,0 +1,111 @@
/* 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 net.waterfox.android.helpers
import android.content.Context
import android.location.Location
import android.location.LocationManager
import android.os.Build
import android.os.SystemClock
import android.util.Log
import androidx.test.core.app.ApplicationProvider
import org.junit.rules.ExternalResource
import java.util.Date
import kotlin.random.Random
import net.waterfox.android.helpers.TestHelper.mDevice
private const val mockProviderName = LocationManager.GPS_PROVIDER
/**
* Rule that sets up a mock location provider that can inject location samples
* straight to the device that the test is running on.
*
* Credit to the mapbox team
* https://github.com/mapbox/mapbox-navigation-android/blob/87fab7ea1152b29533ee121eaf6c05bc202adf02/libtesting-ui/src/main/java/com/mapbox/navigation/testing/ui/MockLocationUpdatesRule.kt
*
*/
class MockLocationUpdatesRule : ExternalResource() {
private val appContext = (ApplicationProvider.getApplicationContext() as Context)
val latitude = Random.nextDouble(-90.0, 90.0)
val longitude = Random.nextDouble(-180.0, 180.0)
private val locationManager: LocationManager by lazy {
(appContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager)
}
override fun before() {
/* ADB command to enable the mock location setting on the device.
* Will not be turned back off due to limitations on knowing its initial state.
*/
mDevice.executeShellCommand(
"appops set " +
appContext.packageName +
" android:mock_location allow"
)
// To mock locations we need a location provider, so we generate and set it here.
try {
locationManager.addTestProvider(
mockProviderName,
false,
false,
false,
false,
true,
true,
true,
3,
2
)
} catch (ex: Exception) {
// unstable
Log.w("MockLocationUpdatesRule", "addTestProvider failed")
}
locationManager.setTestProviderEnabled(mockProviderName, true)
}
// Cleaning up the location provider after the test.
override fun after() {
locationManager.setTestProviderEnabled(mockProviderName, false)
locationManager.removeTestProvider(mockProviderName)
}
/**
* Generate a valid mock location data and set with the help of a test provider.
*
* @param modifyLocation optional callback for modifying the constructed location before setting it.
*/
fun setMockLocation(modifyLocation: (Location.() -> Unit)? = null) {
check(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
"MockLocationUpdatesRule is supported only on Android devices " +
"running version >= Build.VERSION_CODES.M"
}
val location = Location(mockProviderName)
location.time = Date().time
location.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
location.accuracy = 5f
location.altitude = 0.0
location.bearing = 0f
location.speed = 5f
location.latitude = latitude
location.longitude = longitude
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
location.verticalAccuracyMeters = 5f
location.bearingAccuracyDegrees = 5f
location.speedAccuracyMetersPerSecond = 5f
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
location.elapsedRealtimeUncertaintyNanos = 0.0
}
modifyLocation?.let {
location.apply(it)
}
locationManager.setTestProviderLocation(mockProviderName, location)
}
}

View File

@ -0,0 +1,108 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package net.waterfox.android.helpers
import android.net.Uri
import android.os.Handler
import android.os.Looper
import androidx.test.platform.app.InstrumentationRegistry
import okhttp3.mockwebserver.Dispatcher
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
import okio.Buffer
import okio.source
import net.waterfox.android.helpers.ext.toUri
import java.io.IOException
import java.io.InputStream
object MockWebServerHelper {
fun initMockWebServerAndReturnEndpoints(vararg messages: String): List<Uri> {
val mockServer = MockWebServer()
var uniquePath = 0
val uris = mutableListOf<Uri>()
messages.forEach { message ->
val response = MockResponse().setBody("<html><body>$message</body></html>")
mockServer.enqueue(response)
val endpoint = mockServer.url(uniquePath++.toString()).toString().toUri()!!
uris += endpoint
}
return uris
}
/**
* Create a mock webserver that accepts all requests and replies with "OK".
* @return a [MockWebServer] instance
*/
fun createAlwaysOkMockWebServer(): MockWebServer {
return MockWebServer().apply {
val dispatcher = object : Dispatcher() {
@Throws(InterruptedException::class)
override fun dispatch(request: RecordedRequest): MockResponse {
return MockResponse().setBody("OK")
}
}
this.dispatcher = dispatcher
}
}
}
/**
* A [MockWebServer] [Dispatcher] that will return Android assets in the body of requests.
*
* If the dispatcher is unable to read a requested asset, it will fail the test by throwing an
* Exception on the main thread.
*
* @sample [net.waterfox.android.ui.NavigationToolbarTest.visitURLTest]
*/
const val HTTP_OK = 200
const val HTTP_NOT_FOUND = 404
class AndroidAssetDispatcher : Dispatcher() {
private val mainThreadHandler = Handler(Looper.getMainLooper())
override fun dispatch(request: RecordedRequest): MockResponse {
val assetManager = InstrumentationRegistry.getInstrumentation().context.assets
try {
val pathWithoutQueryParams = Uri.parse(request.path!!.drop(1)).path
assetManager.open(pathWithoutQueryParams!!).use { inputStream ->
return fileToResponse(pathWithoutQueryParams, inputStream)
}
} catch (e: IOException) { // e.g. file not found.
// We're on a background thread so we need to forward the exception to the main thread.
mainThreadHandler.postAtFrontOfQueue { throw e }
return MockResponse().setResponseCode(HTTP_NOT_FOUND)
}
}
}
@Throws(IOException::class)
private fun fileToResponse(path: String, file: InputStream): MockResponse {
return MockResponse()
.setResponseCode(HTTP_OK)
.setBody(fileToBytes(file)!!)
.addHeader("content-type: " + contentType(path))
}
@Throws(IOException::class)
private fun fileToBytes(file: InputStream): Buffer? {
val result = Buffer()
result.writeAll(file.source())
return result
}
private fun contentType(path: String): String? {
return when {
path.endsWith(".png") -> "image/png"
path.endsWith(".jpg") -> "image/jpeg"
path.endsWith(".jpeg") -> "image/jpeg"
path.endsWith(".gif") -> "image/gif"
path.endsWith(".svg") -> "image/svg+xml"
path.endsWith(".html") -> "text/html; charset=utf-8"
path.endsWith(".txt") -> "text/plain; charset=utf-8"
else -> "application/octet-stream"
}
}

View File

@ -0,0 +1,32 @@
/* 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 net.waterfox.android.helpers
import androidx.test.espresso.IdlingResource
import androidx.test.espresso.IdlingResource.ResourceCallback
class RecyclerViewIdlingResource constructor(private val recycler: androidx.recyclerview.widget.RecyclerView, val minItemCount: Int = 0) :
IdlingResource {
private var callback: ResourceCallback? = null
override fun isIdleNow(): Boolean {
if (recycler.adapter != null && recycler.adapter!!.itemCount >= minItemCount) {
if (callback != null) {
callback!!.onTransitionToIdle()
}
return true
}
return false
}
override fun registerIdleTransitionCallback(callback: ResourceCallback) {
this.callback = callback
}
override fun getName(): String {
return RecyclerViewIdlingResource::class.java.name + ":" + recycler.id
}
}

View File

@ -0,0 +1,102 @@
/* 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 net.waterfox.android.helpers
import androidx.test.espresso.IdlingResourceTimeoutException
import androidx.test.espresso.NoMatchingViewException
import androidx.test.uiautomator.UiObjectNotFoundException
import junit.framework.AssertionFailedError
import kotlinx.coroutines.runBlocking
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import net.waterfox.android.components.PermissionStorage
import net.waterfox.android.helpers.IdlingResourceHelper.unregisterAllIdlingResources
import net.waterfox.android.helpers.TestHelper.appContext
/**
* Rule to retry flaky tests for a given number of times, catching some of the more common exceptions.
* The Rule doesn't clear the app state in between retries, so we are doing some cleanup here.
* The @Before and @After methods are not called between retries.
*
*/
class RetryTestRule(private val retryCount: Int = 5) : TestRule {
// Used for clearing all permission data after each test try
private val permissionStorage = PermissionStorage(appContext.applicationContext)
@Suppress("TooGenericExceptionCaught", "ComplexMethod")
override fun apply(base: Statement, description: Description): Statement {
return statement {
for (i in 1..retryCount) {
try {
base.evaluate()
break
} catch (t: AssertionError) {
unregisterAllIdlingResources()
runBlocking {
permissionStorage.deleteAllSitePermissions()
}
if (i == retryCount) {
throw t
}
} catch (t: AssertionFailedError) {
unregisterAllIdlingResources()
runBlocking {
permissionStorage.deleteAllSitePermissions()
}
if (i == retryCount) {
throw t
}
} catch (t: UiObjectNotFoundException) {
unregisterAllIdlingResources()
runBlocking {
permissionStorage.deleteAllSitePermissions()
}
if (i == retryCount) {
throw t
}
} catch (t: NoMatchingViewException) {
unregisterAllIdlingResources()
runBlocking {
permissionStorage.deleteAllSitePermissions()
}
if (i == retryCount) {
throw t
}
} catch (t: IdlingResourceTimeoutException) {
unregisterAllIdlingResources()
runBlocking {
permissionStorage.deleteAllSitePermissions()
}
if (i == retryCount) {
throw t
}
} catch (t: RuntimeException) {
unregisterAllIdlingResources()
runBlocking {
permissionStorage.deleteAllSitePermissions()
}
if (i == retryCount) {
throw t
}
} catch (t: NullPointerException) {
unregisterAllIdlingResources()
runBlocking {
permissionStorage.deleteAllSitePermissions()
}
if (i == retryCount) {
throw t
}
}
}
}
}
private inline fun statement(crossinline eval: () -> Unit): Statement {
return object : Statement() {
override fun evaluate() = eval()
}
}
}

View File

@ -0,0 +1,64 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package net.waterfox.android.helpers
import android.os.Handler
import android.os.Looper
import androidx.test.platform.app.InstrumentationRegistry
import java.io.IOException
import java.io.InputStream
import okhttp3.mockwebserver.Dispatcher
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
import okio.Buffer
import okio.source
/**
* A [MockWebServer] [Dispatcher] that will return a generic search results page in the body of
* requests and responds with status 200.
*
* If the dispatcher is unable to read a requested asset, it will fail the test by throwing an
* Exception on the main thread.
*
* @sample [net.waterfox.android.ui.SearchTest]
*/
class SearchDispatcher : Dispatcher() {
private val mainThreadHandler = Handler(Looper.getMainLooper())
override fun dispatch(request: RecordedRequest): MockResponse {
val assetManager = InstrumentationRegistry.getInstrumentation().context.assets
try {
// When we perform a search with the custom search engine, returns the generic4.html test page as search results
if (request.path!!.contains("searchResults.html?search=")) {
MockResponse().setResponseCode(HTTP_OK)
val path = "pages/generic4.html"
assetManager.open(path).use { inputStream ->
return fileToResponse(inputStream)
}
}
return MockResponse().setResponseCode(HTTP_NOT_FOUND)
} catch (e: IOException) {
// e.g. file not found.
// We're on a background thread so we need to forward the exception to the main thread.
mainThreadHandler.postAtFrontOfQueue { throw e }
return MockResponse().setResponseCode(HTTP_NOT_FOUND)
}
}
}
@Throws(IOException::class)
private fun fileToResponse(file: InputStream): MockResponse {
return MockResponse()
.setResponseCode(HTTP_OK)
.setBody(fileToBytes(file))
}
@Throws(IOException::class)
private fun fileToBytes(file: InputStream): Buffer {
val result = Buffer()
result.writeAll(file.source())
return result
}

View File

@ -0,0 +1,48 @@
/* 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 net.waterfox.android.helpers
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.IdlingResource
import mozilla.components.browser.state.selector.selectedTab
import net.waterfox.android.WaterfoxApplication
/**
* An IdlingResource implementation that waits until the current session is not loading anymore.
* Only after loading has completed further actions will be performed.
*/
class SessionLoadedIdlingResource : IdlingResource {
private var resourceCallback: IdlingResource.ResourceCallback? = null
override fun getName(): String {
return SessionLoadedIdlingResource::class.java.simpleName
}
override fun isIdleNow(): Boolean {
val context = ApplicationProvider.getApplicationContext<WaterfoxApplication>()
val selectedTab = context.components.core.store.state.selectedTab
return if (selectedTab?.content?.loading == true) {
false
} else {
if (selectedTab?.content?.progress == 100) {
invokeCallback()
true
} else {
false
}
}
}
private fun invokeCallback() {
if (resourceCallback != null) {
resourceCallback!!.onTransitionToIdle()
}
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
this.resourceCallback = callback
}
}

View File

@ -0,0 +1,123 @@
/* 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 net.waterfox.android.helpers
import android.net.Uri
import okhttp3.mockwebserver.MockWebServer
import net.waterfox.android.helpers.ext.toUri
import java.util.concurrent.TimeUnit
/**
* Helper for hosting web pages locally for testing purposes.
*/
object TestAssetHelper {
@Suppress("MagicNumber")
val waitingTime: Long = TimeUnit.SECONDS.toMillis(15)
val waitingTimeLong = TimeUnit.SECONDS.toMillis(25)
val waitingTimeShort: Long = TimeUnit.SECONDS.toMillis(3)
data class TestAsset(val url: Uri, val content: String, val title: String)
/**
* Hosts 3 simple websites, found at androidTest/assets/pages/generic[1|2|3].html
* Returns a list of TestAsset, which can be used to navigate to each and
* assert that the correct information is being displayed.
*
* Content for these pages all follow the same pattern. See [generic1.html] for
* content implementation details.
*/
fun getGenericAssets(server: MockWebServer): List<TestAsset> {
@Suppress("MagicNumber")
return (1..4).map {
TestAsset(
server.url("pages/generic$it.html").toString().toUri()!!,
"Page content: $it",
""
)
}
}
fun getGenericAsset(server: MockWebServer, pageNum: Int): TestAsset {
val url = server.url("pages/generic$pageNum.html").toString().toUri()!!
val content = "Page content: $pageNum"
val title = "Test_Page_$pageNum"
return TestAsset(url, content, title)
}
fun getLoremIpsumAsset(server: MockWebServer): TestAsset {
val url = server.url("pages/lorem-ipsum.html").toString().toUri()!!
val content = "Page content: lorem ipsum"
return TestAsset(url, content, "")
}
fun getRefreshAsset(server: MockWebServer): TestAsset {
val url = server.url("pages/refresh.html").toString().toUri()!!
val content = "Page content: refresh"
return TestAsset(url, content, "")
}
fun getUUIDPage(server: MockWebServer): TestAsset {
val url = server.url("pages/basic_nav_uuid.html").toString().toUri()!!
val content = "Page content: basic_nav_uuid"
return TestAsset(url, content, "")
}
fun getEnhancedTrackingProtectionAsset(server: MockWebServer): TestAsset {
val url = server.url("pages/trackingPage.html").toString().toUri()!!
val content = "Level 1 (Basic) List"
return TestAsset(url, content, "")
}
fun getImageAsset(server: MockWebServer): TestAsset {
val url = server.url("resources/rabbit.jpg").toString().toUri()!!
return TestAsset(url, "", "")
}
fun getSaveLoginAsset(server: MockWebServer): TestAsset {
val url = server.url("pages/password.html").toString().toUri()!!
return TestAsset(url, "", "")
}
fun getAddressFormAsset(server: MockWebServer): TestAsset {
val url = server.url("pages/addressForm.html").toString().toUri()!!
return TestAsset(url, "", "")
}
fun getCreditCardFormAsset(server: MockWebServer): TestAsset {
val url = server.url("pages/creditCardForm.html").toString().toUri()!!
return TestAsset(url, "", "")
}
fun getAudioPageAsset(server: MockWebServer): TestAsset {
val url = server.url("pages/audioMediaPage.html").toString().toUri()!!
val title = "Audio_Test_Page"
val content = "Page content: audio player"
return TestAsset(url, content, title)
}
fun getVideoPageAsset(server: MockWebServer): TestAsset {
val url = server.url("pages/videoMediaPage.html").toString().toUri()!!
val title = "Video_Test_Page"
val content = "Page content: video player"
return TestAsset(url, content, title)
}
fun getStorageTestAsset(server: MockWebServer, pageAsset: String): TestAsset {
val url = server.url("pages/$pageAsset").toString().toUri()!!
return TestAsset(url, "", "")
}
}

View File

@ -0,0 +1,358 @@
/* 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 net.waterfox.android.helpers
import android.app.ActivityManager
import android.app.PendingIntent
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.view.View
import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assert
import androidx.compose.ui.text.TextLayoutResult
import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.action.ViewActions.longClick
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.toPackage
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import androidx.test.espresso.matcher.ViewMatchers.withChild
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withParent
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject
import androidx.test.uiautomator.UiObjectNotFoundException
import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import junit.framework.AssertionFailedError
import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.support.ktx.android.content.appName
import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.Matcher
import org.junit.Assert
import org.junit.Assert.assertTrue
import net.waterfox.android.R
import net.waterfox.android.customtabs.ExternalAppBrowserActivity
import net.waterfox.android.ext.components
import net.waterfox.android.helpers.Constants.PackageName.GOOGLE_APPS_PHOTOS
import net.waterfox.android.helpers.TestAssetHelper.waitingTime
import net.waterfox.android.helpers.TestAssetHelper.waitingTimeShort
import net.waterfox.android.helpers.ext.waitNotNull
import net.waterfox.android.helpers.idlingresource.NetworkConnectionIdlingResource
import net.waterfox.android.ui.robots.BrowserRobot
import net.waterfox.android.utils.IntentUtils
import java.util.regex.Pattern
object TestHelper {
val appContext: Context = InstrumentationRegistry.getInstrumentation().targetContext
val appName = appContext.appName
var mDevice: UiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
val packageName: String = appContext.packageName
fun scrollToElementByText(text: String): UiScrollable {
val appView = UiScrollable(UiSelector().scrollable(true))
appView.waitForExists(waitingTime)
appView.scrollTextIntoView(text)
return appView
}
fun longTapSelectItem(url: Uri) {
mDevice.waitNotNull(
Until.findObject(By.text(url.toString())),
waitingTime
)
onView(
allOf(
withId(R.id.url),
withText(url.toString())
)
).perform(longClick())
}
fun restartApp(activity: HomeActivityIntentTestRule) {
with(activity) {
finishActivity()
mDevice.waitForIdle()
launchActivity(null)
}
}
fun getPermissionAllowID(): String {
return when
(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
true -> "com.android.permissioncontroller"
false -> "com.android.packageinstaller"
}
}
fun waitUntilObjectIsFound(resourceName: String) {
mDevice.waitNotNull(
Until.findObjects(By.res(resourceName)),
waitingTime
)
}
fun waitUntilSnackbarGone() {
mDevice.findObject(
UiSelector().resourceId("$packageName:id/snackbar_layout"),
).waitUntilGone(waitingTime)
}
fun verifyUrl(urlSubstring: String, resourceName: String, resId: Int) {
waitUntilObjectIsFound(resourceName)
mDevice.findObject(UiSelector().text(urlSubstring)).waitForExists(waitingTime)
onView(withId(resId)).check(ViewAssertions.matches(withText(CoreMatchers.containsString(urlSubstring))))
}
fun openAppFromExternalLink(url: String) {
val context = InstrumentationRegistry.getInstrumentation().getTargetContext()
val intent = Intent().apply {
action = Intent.ACTION_VIEW
data = Uri.parse(url)
`package` = packageName
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
try {
context.startActivity(intent)
} catch (ex: ActivityNotFoundException) {
intent.setPackage(null)
context.startActivity(intent)
}
}
// Remove test file from Google Photos (AOSP) on Firebase
fun deleteDownloadFromStorage() {
val deleteButton = mDevice.findObject(UiSelector().resourceId("$GOOGLE_APPS_PHOTOS:id/trash"))
deleteButton.waitForExists(waitingTime)
deleteButton.click()
// Sometimes there's a secondary confirmation
try {
val deleteConfirm = mDevice.findObject(UiSelector().text("Got it"))
deleteConfirm.waitForExists(waitingTime)
deleteConfirm.click()
} catch (e: UiObjectNotFoundException) {
// Do nothing
}
val trashIt = mDevice.findObject(UiSelector().resourceId("$GOOGLE_APPS_PHOTOS:id/move_to_trash"))
trashIt.waitForExists(waitingTime)
trashIt.click()
}
fun setNetworkEnabled(enabled: Boolean) {
val networkDisconnectedIdlingResource = NetworkConnectionIdlingResource(false)
val networkConnectedIdlingResource = NetworkConnectionIdlingResource(true)
when (enabled) {
true -> {
mDevice.executeShellCommand("svc data enable")
mDevice.executeShellCommand("svc wifi enable")
// Wait for network connection to be completely enabled
IdlingRegistry.getInstance().register(networkConnectedIdlingResource)
Espresso.onIdle {
IdlingRegistry.getInstance().unregister(networkConnectedIdlingResource)
}
}
false -> {
mDevice.executeShellCommand("svc data disable")
mDevice.executeShellCommand("svc wifi disable")
// Wait for network connection to be completely disabled
IdlingRegistry.getInstance().register(networkDisconnectedIdlingResource)
Espresso.onIdle {
IdlingRegistry.getInstance().unregister(networkDisconnectedIdlingResource)
}
}
}
}
fun createCustomTabIntent(
pageUrl: String,
customMenuItemLabel: String = "",
customActionButtonDescription: String = ""
): Intent {
val appContext = InstrumentationRegistry.getInstrumentation()
.targetContext
.applicationContext
val pendingIntent = PendingIntent.getActivity(appContext, 0, Intent(), IntentUtils.defaultIntentPendingFlags)
val customTabsIntent = CustomTabsIntent.Builder()
.addMenuItem(customMenuItemLabel, pendingIntent)
.setShareState(CustomTabsIntent.SHARE_STATE_ON)
.setActionButton(
createTestBitmap(),
customActionButtonDescription, pendingIntent, true
)
.build()
customTabsIntent.intent.data = Uri.parse(pageUrl)
return customTabsIntent.intent
}
private fun createTestBitmap(): Bitmap {
val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
canvas.drawColor(Color.GREEN)
return bitmap
}
fun isPackageInstalled(packageName: String): Boolean {
return try {
val packageManager = InstrumentationRegistry.getInstrumentation().context.packageManager
packageManager.getApplicationInfo(packageName, 0).enabled
} catch (exception: PackageManager.NameNotFoundException) {
false
}
}
fun assertExternalAppOpens(appPackageName: String) {
if (isPackageInstalled(appPackageName)) {
try {
intended(toPackage(appPackageName))
} catch (e: AssertionFailedError) {
e.printStackTrace()
}
} else {
mDevice.waitNotNull(
Until.findObject(By.text("Could not open file")),
waitingTime
)
}
}
fun assertNativeAppOpens(appPackageName: String, url: String = "") {
if (isPackageInstalled(appPackageName)) {
mDevice.waitForIdle(waitingTimeShort)
assertTrue(
mDevice.findObject(UiSelector().packageName(appPackageName))
.waitForExists(waitingTime)
)
} else {
BrowserRobot().verifyUrl(url)
}
}
/**
* Checks whether the latest activity of the application is used for custom tabs or PWAs.
*
* @return Boolean value that helps us know if the current activity supports custom tabs or PWAs.
*/
fun isExternalAppBrowserActivityInCurrentTask(): Boolean {
val activityManager = appContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
mDevice.waitForIdle(waitingTimeShort)
return activityManager.appTasks[0].taskInfo.topActivity!!.className == ExternalAppBrowserActivity::class.java.name
}
// exit from Menus to home screen or browser
fun exitMenu() {
val toolbar =
mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
while (!toolbar.waitForExists(waitingTimeShort)) {
mDevice.pressBack()
}
}
fun UiDevice.waitForObjects(obj: UiObject, waitingTime: Long = TestAssetHelper.waitingTime) {
this.waitForIdle()
Assert.assertNotNull(obj.waitForExists(waitingTime))
}
fun hasCousin(matcher: Matcher<View>): Matcher<View> {
return withParent(
hasSibling(
withChild(
matcher
)
)
)
}
fun getStringResource(id: Int) = appContext.resources.getString(id, appName)
fun setCustomSearchEngine(searchEngine: SearchEngine) {
with(appContext.components.useCases.searchUseCases) {
addSearchEngine(searchEngine)
selectSearchEngine(searchEngine)
}
}
fun grantPermission() {
if (Build.VERSION.SDK_INT >= 23) {
mDevice.findObject(
By.text(
when (Build.VERSION.SDK_INT) {
Build.VERSION_CODES.R -> Pattern.compile(
"WHILE USING THE APP", Pattern.CASE_INSENSITIVE
)
else -> Pattern.compile("Allow", Pattern.CASE_INSENSITIVE)
}
)
).click()
}
}
fun denyPermission() {
if (Build.VERSION.SDK_INT >= 23) {
mDevice.findObject(
By.text(
when (Build.VERSION.SDK_INT) {
Build.VERSION_CODES.R -> Pattern.compile(
"DENY", Pattern.CASE_INSENSITIVE
)
else -> Pattern.compile("Deny", Pattern.CASE_INSENSITIVE)
}
)
).click()
}
}
private val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
fun generateRandomString(stringLength: Int) =
(1..stringLength)
.map { kotlin.random.Random.nextInt(0, charPool.size) }
.map(charPool::get)
.joinToString("")
fun SemanticsNodeInteraction.assertTextColor(
color: androidx.compose.ui.graphics.Color
): SemanticsNodeInteraction = assert(isOfColor(color))
private fun isOfColor(color: androidx.compose.ui.graphics.Color): SemanticsMatcher = SemanticsMatcher(
"${SemanticsProperties.Text.name} is of color '$color'"
) {
val textLayoutResults = mutableListOf<TextLayoutResult>()
it.config.getOrNull(SemanticsActions.GetTextLayoutResult)
?.action
?.invoke(textLayoutResults)
return@SemanticsMatcher if (textLayoutResults.isEmpty()) {
false
} else {
textLayoutResults.first().layoutInput.style.color == color
}
}
}

View File

@ -0,0 +1,36 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package net.waterfox.android.helpers
import android.view.View
import androidx.test.espresso.IdlingResource
class ViewVisibilityIdlingResource(
private val view: View,
private val expectedVisibility: Int
) : IdlingResource {
private var resourceCallback: IdlingResource.ResourceCallback? = null
private var isIdle: Boolean = false
override fun getName(): String {
return ViewVisibilityIdlingResource::class.java.name + ":" + view.id + ":" + expectedVisibility
}
override fun isIdleNow(): Boolean {
if (isIdle) return true
isIdle = view.visibility == expectedVisibility
if (isIdle) {
resourceCallback?.onTransitionToIdle()
}
return isIdle
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
this.resourceCallback = callback
}
}

View File

@ -0,0 +1,45 @@
/* 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 net.waterfox.android.helpers.ext
import android.net.Uri
import java.net.URI
import java.net.URISyntaxException
// Extension functions for the String class
/**
* If this string starts with the one or more of the given [prefixes] (in order and ignoring case),
* returns a copy of this string with the prefixes removed. Otherwise, returns this string.
*/
fun String.removePrefixesIgnoreCase(vararg prefixes: String): String {
var value = this
var lower = this.lowercase()
prefixes.forEach {
if (lower.startsWith(it.lowercase())) {
value = value.substring(it.length)
lower = lower.substring(it.length)
}
}
return value
}
fun String?.toUri(): Uri? = if (this == null) {
null
} else {
Uri.parse(this)
}
fun String?.toJavaURI(): URI? = if (this == null) {
null
} else {
try {
URI(this)
} catch (e: URISyntaxException) {
null
}
}

View File

@ -0,0 +1,48 @@
/* 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 net.waterfox.android.helpers
import android.view.InputDevice
import android.view.MotionEvent
import androidx.test.espresso.ViewAction
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.GeneralClickAction
import androidx.test.espresso.action.GeneralLocation
import androidx.test.espresso.action.Press
import androidx.test.espresso.action.Tap
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions.matches
fun ViewInteraction.click(): ViewInteraction = this.perform(ViewActions.click())!!
fun ViewInteraction.assertIsEnabled(isEnabled: Boolean): ViewInteraction {
return this.check(matches(isEnabled(isEnabled)))!!
}
fun ViewInteraction.assertIsChecked(isChecked: Boolean): ViewInteraction {
return this.check(matches(isChecked(isChecked)))!!
}
fun ViewInteraction.assertIsSelected(isSelected: Boolean): ViewInteraction {
return this.check(matches(isSelected(isSelected)))!!
}
/**
* Perform a click (simulate the finger touching the View) at a specific location in the View
* rather than the default middle of the View.
*
* Useful in situations where the View we want clicked contains other Views in it's x,y middle
* and we need to simulate the touch in some other free space of the View we want clicked.
*/
fun ViewInteraction.clickAtLocationInView(locationInView: GeneralLocation): ViewAction =
ViewActions.actionWithAssertions(
GeneralClickAction(
Tap.SINGLE,
locationInView,
Press.FINGER,
InputDevice.SOURCE_UNKNOWN,
MotionEvent.BUTTON_PRIMARY
)
)

View File

@ -0,0 +1,20 @@
/* 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 net.waterfox.android.helpers.ext
import androidx.test.uiautomator.SearchCondition
import androidx.test.uiautomator.UiDevice
import org.junit.Assert.assertNotNull
import net.waterfox.android.helpers.TestAssetHelper
/**
* Blocks the test for [waitTime] miliseconds before continuing.
*
* Will cause the test to fail is the condition is not met before the timeout.
*/
fun UiDevice.waitNotNull(
searchCondition: SearchCondition<*>,
waitTime: Long = TestAssetHelper.waitingTime
) = assertNotNull(wait(searchCondition, waitTime))

View File

@ -0,0 +1,46 @@
/* 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 net.waterfox.android.helpers.idlingresource
import androidx.fragment.app.FragmentManager
import androidx.navigation.fragment.NavHostFragment
import androidx.test.espresso.IdlingResource
import mozilla.components.feature.addons.ui.AddonInstallationDialogFragment
class AddonsInstallingIdlingResource(
private val fragmentManager: FragmentManager
) :
IdlingResource {
private var resourceCallback: IdlingResource.ResourceCallback? = null
private var isAddonInstalled = false
override fun getName(): String {
return this::javaClass.name
}
override fun isIdleNow(): Boolean {
return isInstalledAddonDialogShown()
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
if (callback != null)
resourceCallback = callback
}
private fun isInstalledAddonDialogShown(): Boolean {
val activityChildFragments =
(fragmentManager.fragments.first() as NavHostFragment)
.childFragmentManager.fragments
for (childFragment in activityChildFragments.indices) {
if (activityChildFragments[childFragment] is AddonInstallationDialogFragment) {
resourceCallback?.onTransitionToIdle()
isAddonInstalled = true
return isAddonInstalled
}
}
return isAddonInstalled
}
}

View File

@ -0,0 +1,45 @@
/* 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 net.waterfox.android.helpers.idlingresource
import android.view.View
import android.view.View.VISIBLE
import androidx.fragment.app.FragmentManager
import androidx.test.espresso.IdlingResource
import net.waterfox.android.R
import net.waterfox.android.addons.AddonsManagementFragment
class AddonsLoadingIdlingResource(val fragmentManager: FragmentManager) : IdlingResource {
private var resourceCallback: IdlingResource.ResourceCallback? = null
override fun getName(): String {
return this::javaClass.name
}
override fun isIdleNow(): Boolean {
val idle = addonsFinishedLoading()
if (idle)
resourceCallback?.onTransitionToIdle()
return idle
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
if (callback != null)
resourceCallback = callback
}
private fun addonsFinishedLoading(): Boolean {
val progressbar = fragmentManager.findFragmentById(R.id.container)?.let {
val addonsManagementFragment =
it.childFragmentManager.fragments.first { it is AddonsManagementFragment }
addonsManagementFragment.view?.findViewById<View>(R.id.add_ons_progress_bar)
} ?: return true
if (progressbar.visibility == VISIBLE)
return false
return true
}
}

View File

@ -0,0 +1,56 @@
/* 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 net.waterfox.android.helpers.idlingresource
import android.view.View
import androidx.test.espresso.IdlingResource
import androidx.test.espresso.IdlingResource.ResourceCallback
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
class BottomSheetBehaviorStateIdlingResource(behavior: BottomSheetBehavior<*>) :
BottomSheetCallback(), IdlingResource {
private var isIdle: Boolean
private var callback: ResourceCallback? = null
override fun onStateChanged(bottomSheet: View, newState: Int) {
val wasIdle = isIdle
isIdle = isIdleState(newState)
if (!wasIdle && isIdle && callback != null) {
callback!!.onTransitionToIdle()
}
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {
// no-op
}
override fun getName(): String {
return BottomSheetBehaviorStateIdlingResource::class.java.simpleName
}
override fun isIdleNow(): Boolean {
return isIdle
}
override fun registerIdleTransitionCallback(callback: ResourceCallback) {
this.callback = callback
}
private fun isIdleState(state: Int): Boolean {
return state != BottomSheetBehavior.STATE_DRAGGING &&
state != BottomSheetBehavior.STATE_SETTLING &&
// When detecting STATE_HALF_EXPANDED we immediately transit to STATE_HIDDEN.
// Consider this also an intermediary state so not idling.
state != BottomSheetBehavior.STATE_HALF_EXPANDED
}
init {
behavior.addBottomSheetCallback(this)
val state = behavior.state
isIdle = isIdleState(state)
}
}

View File

@ -0,0 +1,48 @@
/* 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 net.waterfox.android.helpers.idlingresource
import android.net.ConnectivityManager
import androidx.core.content.getSystemService
import androidx.test.espresso.IdlingResource
import androidx.test.platform.app.InstrumentationRegistry
import net.waterfox.android.ext.isOnline
/**
* An IdlingResource implementation that waits until the network connection is online or offline.
* The networkConnected parameter sets the expected connection status.
* Only after connecting/disconnecting has completed further actions will be performed.
*/
class NetworkConnectionIdlingResource(private val networkConnected: Boolean) : IdlingResource {
private var resourceCallback: IdlingResource.ResourceCallback? = null
private val connectionManager =
InstrumentationRegistry.getInstrumentation().context.getSystemService<ConnectivityManager>()
override fun getName(): String {
return this::javaClass.name
}
override fun isIdleNow(): Boolean {
val idle =
if (networkConnected) {
isOnline()
} else {
!isOnline()
}
if (idle)
resourceCallback?.onTransitionToIdle()
return idle
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
if (callback != null)
resourceCallback = callback
}
private fun isOnline(): Boolean {
return connectionManager!!.isOnline()
}
}

View File

@ -0,0 +1,39 @@
/* 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 net.waterfox.android.helpers.matchers
import android.graphics.Bitmap
import android.view.View
import android.widget.ImageView
import androidx.test.espresso.matcher.BoundedMatcher
import org.hamcrest.Description
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.StateListDrawable
import android.graphics.drawable.Drawable
class BitmapDrawableMatcher(private val bitmap: Bitmap, private val name: String) :
BoundedMatcher<View, ImageView>(ImageView::class.java) {
override fun describeTo(description: Description?) {
description?.appendText("has image drawable resource $name")
}
override fun matchesSafely(item: ImageView): Boolean {
return sameBitmap(item.drawable, bitmap)
}
private fun sameBitmap(drawable: Drawable?, otherBitmap: Bitmap): Boolean {
var currentDrawable = drawable ?: return false
if (currentDrawable is StateListDrawable) {
currentDrawable = currentDrawable.current
}
if (currentDrawable is BitmapDrawable) {
val bitmap = currentDrawable.bitmap
return bitmap.sameAs(otherBitmap)
}
return false
}
}

View File

@ -0,0 +1,39 @@
/* 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 net.waterfox.android.helpers.matchers
import android.view.View
import androidx.test.espresso.matcher.BoundedMatcher
import com.google.android.material.bottomsheet.BottomSheetBehavior
import org.hamcrest.Description
class BottomSheetBehaviorStateMatcher(private val expectedState: Int) :
BoundedMatcher<View, View>(View::class.java) {
override fun describeTo(description: Description?) {
description?.appendText("BottomSheetBehavior in state: \"$expectedState\"")
}
override fun matchesSafely(item: View): Boolean {
val behavior = BottomSheetBehavior.from(item)
return behavior.state == expectedState
}
}
class BottomSheetBehaviorHalfExpandedMaxRatioMatcher(private val maxHalfExpandedRatio: Float) :
BoundedMatcher<View, View>(View::class.java) {
override fun describeTo(description: Description?) {
description?.appendText(
"BottomSheetBehavior with an at max halfExpandedRation: " +
"$maxHalfExpandedRatio"
)
}
override fun matchesSafely(item: View): Boolean {
val behavior = BottomSheetBehavior.from(item)
return behavior.halfExpandedRatio <= maxHalfExpandedRatio
}
}

View File

@ -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 net.waterfox.android.helpers.matchers
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.matcher.BoundedMatcher
import org.hamcrest.Description
import org.hamcrest.Matcher
fun hasItem(matcher: Matcher<View?>): Matcher<View?>? {
return object : BoundedMatcher<View?, RecyclerView>(RecyclerView::class.java) {
override fun describeTo(description: Description) {
description.appendText("has item: ")
matcher.describeTo(description)
}
override fun matchesSafely(view: RecyclerView): Boolean {
val adapter = view.adapter
for (position in 0 until adapter!!.itemCount) {
val type = adapter.getItemViewType(position)
val holder = adapter.createViewHolder(view, type)
adapter.onBindViewHolder(holder, position)
if (matcher.matches(holder.itemView)) {
return true
}
}
return false
}
}
}

View File

@ -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 http://mozilla.org/MPL/2.0/. */
package net.waterfox.android.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++
}
}
}

View File

@ -0,0 +1,159 @@
/* 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 net.waterfox.android.perf
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import net.waterfox.android.ext.components
import net.waterfox.android.helpers.HomeActivityTestRule
// BEFORE CHANGING EXPECTED_* VALUES, PLEASE READ THE TEST CLASS KDOC.
/**
* The number of times a StrictMode violation is suppressed during this start up scenario.
* Incrementing the expected value indicates a potential performance regression.
*
* One feature of StrictMode is to detect potential performance regressions and, in particular, to
* detect main thread IO. This includes network requests (which can block for multiple seconds),
* file read/writes (which generally block for tens to hundreds of milliseconds), and file stats
* (like most SharedPreferences accesses, which block for small amounts of time). Main thread IO
* should be replaced with a background operation that posts to the main thread when the IO request
* is complete.
*
* Say no to main thread IO! 🙅
*/
private const val EXPECTED_SUPPRESSION_COUNT = 15
/**
* The number of times we call the `runBlocking` coroutine method on the main thread during this
* start up scenario. Increment the expected values indicates a potential performance regression.
*
* runBlocking indicates that we're blocking the current thread waiting for the result of another
* coroutine. While the main thread is blocked, 1) we can't handle user input and the user may feel
* Waterfox is slow and 2) we can't use the main thread to continue initialization that must occur on
* the main thread (like initializing UI), slowing down start up overall. Blocking calls should
* generally be replaced with a slow operation on a background thread launching onto the main thread
* when completed. However, in a very small number of cases, blocking may be impossible to avoid.
*/
private val EXPECTED_RUNBLOCKING_RANGE = 0..1 // CI has +1 counts compared to local runs: increment these together
/**
* The number of `ConstraintLayout`s we inflate that are children of a `RecyclerView` during this
* start up scenario. Incrementing the expected value indicates a potential performance regression.
* THIS IS AN EXPERIMENTAL METRIC and we are not yet confident reducing this count will mitigate
* start up regressions. If you do not find it useful or if it's too noisy, you can consider
* removing it.
*
* ConstraintLayout is expensive to inflate (though fast to measure/layout) so we want to avoid
* creating too many of them synchronously during start up. Generally, these should be inflated
* asynchronously or replaced with cheaper layouts (if they're not too expensive to measure/layout).
* If the view hierarchy uses Jetpack Compose, switching to that is also an option.
*/
private val EXPECTED_RECYCLER_VIEW_CONSTRAINT_LAYOUT_CHILDREN =
4..6 // The messaging framework is not deterministic and could add to the count.
/**
* The number of layouts we inflate during this start up scenario. Incrementing the expected value
* indicates a potential performance regression. THIS IS AN EXPERIMENTAL METRIC and we are not yet
* confident reducing this count will mitigate start up regressions. If you do not find it useful or
* if it's too noisy, you can consider removing it.
*
* Each layout inflation is suspected of having overhead (e.g. accessing each layout resource from
* disk) so suspect inflating more layouts may slow down start up. Ideally, layouts would be merged
* such that there is one inflation that includes all of the views needed on start up.
*/
private val EXPECTED_NUMBER_OF_INFLATION =
13..14 // The messaging framework is not deterministic and could add a +1 to the count
private val failureMsgStrictMode = getErrorMessage("StrictMode suppression")
private val failureMsgRunBlocking = getErrorMessage("runBlockingIncrement")
private val failureMsgRecyclerViewConstraintLayoutChildren = getErrorMessage(
"ConstraintLayout being a common direct descendant of a RecyclerView"
)
private val failureMsgNumberOfInflation = getErrorMessage("start up inflation")
/**
* A performance test that attempts to minimize start up performance regressions using heuristics
* rather than benchmarking. These heuristics measure occurrences of known performance anti-patterns
* and fails when the occurrence count changes. If the change indicates a regression, we should
* re-evaluate the PR to see if we can avoid the potential regression and, if not, change the
* expected value. If it indicates an improvement, we can change the expected value. The expected
* values can be updated without consulting the performance team.
*
* See `EXPECTED_*` above for explanations of the heuristics this test currently supports.
*
* The benefits of a heuristics-based performance test are that it is uses less CI time to get
* results so we can run it more often (e.g. for each PR) and it is less noisy than a benchmark.
* However, the downsides of this style of test is that if a heuristic value increases, it may not
* represent a real, significant performance regression.
*/
class StartupExcessiveResourceUseTest {
@get:Rule
val activityTestRule = HomeActivityTestRule(skipOnboarding = true)
private val uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@Test
fun verifyRunBlockingAndStrictModeSuppresionCount() {
uiDevice.waitForIdle() // wait for async UI to load.
// This might cause intermittents: at an arbitrary point after start up (such as the visual
// completeness queue), we might run code on the main thread that suppresses StrictMode,
// causing this number to fluctuate depending on device speed. We'll deal with it if it occurs.
val actualSuppresionCount = activityTestRule.activity.components.strictMode.suppressionCount.get().toInt()
val actualRunBlocking = RunBlockingCounter.count.get()
assertEquals(failureMsgStrictMode, EXPECTED_SUPPRESSION_COUNT, actualSuppresionCount)
assertTrue(failureMsgRunBlocking + "actual: $actualRunBlocking", actualRunBlocking in EXPECTED_RUNBLOCKING_RANGE)
// This below asserts fail in Firebase with different values for
// "actualRecyclerViewConstraintLayoutChildren" or "actualNumberOfInflations"
// See https://github.com/mozilla-mobile/fenix/pull/26512 and https://github.com/mozilla-mobile/fenix/issues/25142
//
// val rootView = activityTestRule.activity.findViewById<LinearLayout>(R.id.rootContainer)
// val actualRecyclerViewConstraintLayoutChildren = countRecyclerViewConstraintLayoutChildren(rootView, null)
// assertTrue(
// failureMsgRecyclerViewConstraintLayoutChildren + "actual: $actualRecyclerViewConstraintLayoutChildren",
// actualRecyclerViewConstraintLayoutChildren in EXPECTED_RECYCLER_VIEW_CONSTRAINT_LAYOUT_CHILDREN,
// )
// val actualNumberOfInflations = InflationCounter.inflationCount.get()
// assertTrue(
// failureMsgNumberOfInflation + "actual: $actualNumberOfInflations",
// actualNumberOfInflations in EXPECTED_NUMBER_OF_INFLATION,
// )
}
}
private fun countRecyclerViewConstraintLayoutChildren(view: View, parent: View?): Int {
val viewValue = if (parent is RecyclerView && view is ConstraintLayout) {
1
} else {
0
}
return if (view !is ViewGroup) {
viewValue
} else {
viewValue + view.children.sumOf { countRecyclerViewConstraintLayoutChildren(it, view) }
}
}
private fun getErrorMessage(shortName: String) = """$shortName count does not match expected count.
This heuristic-based performance test is expected measure the number of occurrences of known
performance anti-patterns and fail when that count changes. Please read the class documentation
for more details about this test and an explanation of what the failed heuristic is expected to
measure. Please consult the performance team if you have questions.
"""

View File

@ -0,0 +1,73 @@
/* 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/. */
@file:Suppress("DEPRECATION")
package net.waterfox.android.screenshots
import android.os.SystemClock
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import androidx.test.uiautomator.UiDevice
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import net.waterfox.android.helpers.HomeActivityTestRule
import net.waterfox.android.HomeActivity
import net.waterfox.android.helpers.TestAssetHelper
import net.waterfox.android.ui.robots.homeScreen
import tools.fastlane.screengrab.Screengrab
import tools.fastlane.screengrab.locale.LocaleTestRule
class DefaultHomeScreenTest : ScreenshotTest() {
private lateinit var mDevice: UiDevice
@Rule @JvmField
val localeTestRule = LocaleTestRule()
@get:Rule
var mActivityTestRule: ActivityTestRule<HomeActivity> = HomeActivityTestRule()
@Before
fun setUp() {
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
}
@After
fun tearDown() {
mActivityTestRule.getActivity().finishAndRemoveTask()
}
@Test
fun showDefaultHomeScreen() {
homeScreen {
swipeToBottom()
verifyAccountsSignInButton()
Screengrab.screenshot("HomeScreenRobot_home-screen-scroll")
TestAssetHelper.waitingTime
}
}
@Test
fun privateBrowsingTest() {
homeScreen {
SystemClock.sleep(TestAssetHelper.waitingTimeShort)
Screengrab.screenshot("HomeScreenRobot_home-screen")
}.openThreeDotMenu {
}.openSettings {
}.openPrivateBrowsingSubMenu {
clickPrivateModeScreenshotsSwitch()
}
// To get private screenshot,
// dismiss onboarding going to settings and back
mDevice.pressBack()
mDevice.pressBack()
homeScreen {
togglePrivateBrowsingModeOnOff()
Screengrab.screenshot("HomeScreenRobot_private-browsing-menu")
togglePrivateBrowsingModeOnOff()
Screengrab.screenshot("HomeScreenRobot_after-onboarding")
}
}
}

View File

@ -0,0 +1,235 @@
/* 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/. */
@file:Suppress("DEPRECATION")
package net.waterfox.android.screenshots
import android.os.SystemClock
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import net.waterfox.android.R
import net.waterfox.android.helpers.*
import net.waterfox.android.helpers.TestHelper.mDevice
import net.waterfox.android.helpers.ext.waitNotNull
import net.waterfox.android.ui.robots.bookmarksMenu
import net.waterfox.android.ui.robots.homeScreen
import net.waterfox.android.ui.robots.navigationToolbar
import net.waterfox.android.ui.robots.swipeToBottom
import okhttp3.mockwebserver.MockWebServer
import org.junit.*
import tools.fastlane.screengrab.Screengrab
import tools.fastlane.screengrab.locale.LocaleTestRule
class MenuScreenShotTest : ScreenshotTest() {
private lateinit var mockWebServer: MockWebServer
private lateinit var mDevice: UiDevice
private val featureSettingsHelper = FeatureSettingsHelper()
@Rule
@JvmField
val localeTestRule = LocaleTestRule()
@get:Rule
val composeTestRule = AndroidComposeTestRule(
HomeActivityTestRule()
) { it.activity }
private val activity by lazy { composeTestRule.activity }
@Before
fun setUp() {
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
featureSettingsHelper.setTCPCFREnabled(false)
}
@After
fun tearDown() {
featureSettingsHelper.resetAllFeatureFlags()
activity.finishAndRemoveTask()
mockWebServer.shutdown()
}
@Test
fun threeDotMenuTest() {
homeScreen {
}.openThreeDotMenu {
Screengrab.screenshot("ThreeDotMenuMainRobot_three-dot-menu")
}
}
@Test
fun settingsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
Screengrab.screenshot("SettingsRobot_settings-menu")
}.openTurnOnSyncMenu {
Screengrab.screenshot("AccountSettingsRobot_settings-account")
}.goBack {
}.openSearchSubMenu {
Screengrab.screenshot("SettingsSubMenuSearchRobot_settings-search")
}.goBack {
}.openCustomizeSubMenu {
Screengrab.screenshot("SettingsSubMenuThemeRobot_settings-theme")
}.goBack {
}.openAccessibilitySubMenu {
Screengrab.screenshot("SettingsSubMenuAccessibilityRobot_settings-accessibility")
}.goBack {
}.openLanguageSubMenu {
Screengrab.screenshot("SettingsSubMenuAccessibilityRobot_settings-language")
}.goBack {
// From about here we need to scroll up to ensure all settings options are visible.
}.openSetDefaultBrowserSubMenu {
Screengrab.screenshot("SettingsSubMenuDefaultBrowserRobot_settings-default-browser")
}.goBack {
// Disabled for Pixel 2
// }.openEnhancedTrackingProtectionSubMenu {
// Screengrab.screenshot("settings-enhanced-tp")
// }.goBack {
}.openLoginsAndPasswordSubMenu {
Screengrab.screenshot("SettingsSubMenuLoginsAndPasswords-settings-logins-passwords")
}.goBack {
swipeToBottom()
Screengrab.screenshot("SettingsRobot_settings-scroll-to-bottom")
}.openAddonsManagerMenu {
Screengrab.screenshot("settings-addons")
}
}
@Test
fun historyTest() {
homeScreen {
}.openThreeDotMenu {
}
openHistoryThreeDotMenu()
Screengrab.screenshot("HistoryRobot_history-menu")
}
@Test
fun bookmarksManagementTest() {
homeScreen {
}.openThreeDotMenu {
}
openBookmarksThreeDotMenu()
Screengrab.screenshot("BookmarksRobot_bookmarks-menu")
bookmarksMenu {
clickAddFolderButtonUsingId()
Screengrab.screenshot("BookmarksRobot_add-folder-view")
saveNewFolder()
Screengrab.screenshot("BookmarksRobot_error-empty-folder-name")
addNewFolderName("test", composeTestRule)
saveNewFolder()
}.openThreeDotMenu("test", composeTestRule) {
Screengrab.screenshot("ThreeDotMenuBookmarksRobot_folder-menu")
}
editBookmarkFolder()
Screengrab.screenshot("ThreeDotMenuBookmarksRobot_edit-bookmark-folder-menu")
// It may be needed to wait here to have the screenshot
bookmarksMenu {
navigateUp()
}.openThreeDotMenu("test", composeTestRule) {
deleteBookmarkFolder()
Screengrab.screenshot("ThreeDotMenuBookmarksRobot_delete-bookmark-folder-menu")
}
}
@Test
fun collectionMenuTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
Screengrab.screenshot("NavigationToolbarRobot_navigation-toolbar")
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
Screengrab.screenshot("BrowserRobot_enter-url")
}.openTabDrawer {
TestAssetHelper.waitingTime
Screengrab.screenshot("TabDrawerRobot_one-tab-open")
}.openTabsListThreeDotMenu {
TestAssetHelper.waitingTime
Screengrab.screenshot("TabDrawerRobot_three-dot-menu")
}
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun tabMenuTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
Screengrab.screenshot("TabDrawerRobot_browser-tab-menu")
}.closeBrowserMenuToBrowser {
}.openTabDrawer {
Screengrab.screenshot("TabDrawerRobot_tab-drawer-with-tabs")
closeTab()
TestAssetHelper.waitingTime
Screengrab.screenshot("TabDrawerRobot_remove-tab")
}
}
@Test
fun saveLoginPromptTest() {
val saveLoginTest =
TestAssetHelper.getSaveLoginAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(saveLoginTest.url) {
verifySaveLoginPromptIsShownNotSave()
SystemClock.sleep(TestAssetHelper.waitingTimeShort)
Screengrab.screenshot("save-login-prompt")
}
}
}
fun openHistoryThreeDotMenu() = onView(withText(R.string.library_history)).click()
fun openBookmarksThreeDotMenu() = onView(withText(R.string.library_bookmarks)).click()
fun editBookmarkFolder() = onView(withText(R.string.bookmark_menu_edit_button)).click()
fun deleteBookmarkFolder() = onView(withText(R.string.bookmark_menu_delete_button)).click()
fun tapOnTabCounter() = onView(withId(R.id.counter_text)).click()
fun settingsAccountPreferences() = onView(withText(R.string.preferences_sync_2)).click()
fun settingsSearch() = onView(withText(R.string.preferences_search)).click()
fun settingsTheme() = onView(withText(R.string.preferences_customize)).click()
fun settingsAccessibility() = onView(withText(R.string.preferences_accessibility)).click()
fun settingDefaultBrowser() = onView(withText(R.string.preferences_set_as_default_browser)).click()
fun settingsToolbar() = onView(withText(R.string.preferences_toolbar)).click()
fun settingsTP() = onView(withText(R.string.preference_enhanced_tracking_protection)).click()
fun settingsAddToHomeScreen() = onView(withText(R.string.preferences_add_private_browsing_shortcut)).click()
fun settingsRemoveData() = onView(withText(R.string.preferences_delete_browsing_data)).click()
fun loginsAndPassword() = onView(withText(R.string.preferences_passwords_logins_and_passwords)).click()
fun addOns() = onView(withText(R.string.preferences_addons)).click()
fun settingsLanguage() = onView(withText(R.string.preferences_language)).click()
fun verifySaveLoginPromptIsShownNotSave() {
mDevice.waitNotNull(Until.findObjects(By.text("test@example.com")), TestAssetHelper.waitingTime)
val submitButton = mDevice.findObject(By.res("submit"))
submitButton.clickAndWait(Until.newWindow(), TestAssetHelper.waitingTime)
}
fun clickAddFolderButtonUsingId() = onView(withId(R.id.add_bookmark_folder)).click()

View File

@ -0,0 +1,73 @@
/* 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 net.waterfox.android.screenshots;
import android.Manifest;
import android.app.Instrumentation;
import android.content.Context;
import androidx.annotation.StringRes;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.uiautomator.UiDevice;
import org.junit.Before;
import org.junit.Rule;
import org.junit.rules.TestRule;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import tools.fastlane.screengrab.Screengrab;
import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy;
/**
* Base class for tests that take screenshots.
*/
public abstract class ScreenshotTest {
private Context targetContext;
UiDevice device;
@Rule
public GrantPermissionRule permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE);
@Rule
public TestRule screenshotOnFailureRule = new TestWatcher() {
@Override
protected void failed(Throwable e, Description description) {
// On error take a screenshot so that we can debug it easily
Screengrab.screenshot("FAILURE-" + getScreenshotName(description));
}
private String getScreenshotName(Description description) {
return description.getClassName().replace(".", "-")
+ "_"
+ description.getMethodName().replace(".", "-");
}
};
@Before
public void setUpScreenshots() {
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
targetContext = instrumentation.getTargetContext();
device = UiDevice.getInstance(instrumentation);
// Use this to switch between default strategy and HostScreencap strategy
Screengrab.setDefaultScreenshotStrategy(new UiAutomatorScreenshotStrategy());
}
String getString(@StringRes int resourceId) {
return targetContext.getString(resourceId).trim();
}
String getString(@StringRes int resourceId, Object... formatArgs) {
return targetContext.getString(resourceId, formatArgs).trim();
}
public void takeScreenshotsAfterWait(String filename, int waitingTime) throws InterruptedException {
Thread.sleep(waitingTime);
Screengrab.screenshot(filename);
}
}

View File

@ -0,0 +1,22 @@
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"
[packages]
fxapom = "*"
mozdownload = "*"
mozinstall = "*"
mozprofile = "*"
mozrunner = "*"
mozversion = "*"
pytest = "*"
pytest-fxa = "*"
pytest-html = "*"
pytest-metadata = "*"
requests = "*"
[dev-packages]
[requires]
python_version = "2.7"

View File

@ -0,0 +1,673 @@
{
"_meta": {
"hash": {
"sha256": "112a12fa2e9e8117b399b60a49b4c8799a614ef655992640c95149bf95f33e8b"
},
"pipfile-spec": 6,
"requires": {
"python_version": "2.7"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.python.org/simple",
"verify_ssl": true
}
]
},
"default": {
"atomicwrites": {
"hashes": [
"sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197",
"sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"
],
"version": "==1.4.0"
},
"attrs": {
"hashes": [
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
],
"version": "==20.3.0"
},
"backports.functools-lru-cache": {
"hashes": [
"sha256:0bada4c2f8a43d533e4ecb7a12214d9420e66eb206d54bf2d682581ca4b80848",
"sha256:8fde5f188da2d593bd5bc0be98d9abc46c95bb8a9dde93429570192ee6cc2d4a"
],
"markers": "python_version < '3.2'",
"version": "==1.6.1"
},
"blessings": {
"hashes": [
"sha256:98e5854d805f50a5b58ac2333411b0482516a8210f23f43308baeb58d77c157d",
"sha256:b1fdd7e7a675295630f9ae71527a8ebc10bfefa236b3d6aa4932ee4462c17ba3",
"sha256:caad5211e7ba5afe04367cdd4cfc68fa886e2e08f6f35e76b7387d2109ccea6e"
],
"version": "==1.7"
},
"certifi": {
"hashes": [
"sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
"sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
],
"version": "==2020.12.5"
},
"cffi": {
"hashes": [
"sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e",
"sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d",
"sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a",
"sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec",
"sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362",
"sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668",
"sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c",
"sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b",
"sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06",
"sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698",
"sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2",
"sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c",
"sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7",
"sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009",
"sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03",
"sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b",
"sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909",
"sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53",
"sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35",
"sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26",
"sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b",
"sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01",
"sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb",
"sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293",
"sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd",
"sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d",
"sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3",
"sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d",
"sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e",
"sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca",
"sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d",
"sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775",
"sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375",
"sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b",
"sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b",
"sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f"
],
"version": "==1.14.4"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"configparser": {
"hashes": [
"sha256:254c1d9c79f60c45dfde850850883d5aaa7f19a23f13561243a050d5a7c3fe4c",
"sha256:c7d282687a5308319bf3d2e7706e575c635b0a470342641c93bea0ea3b5331df"
],
"markers": "python_version < '3'",
"version": "==4.0.2"
},
"contextlib2": {
"hashes": [
"sha256:01f490098c18b19d2bd5bb5dc445b2054d2fa97f09a4280ba2c5f3c394c8162e",
"sha256:3355078a159fbb44ee60ea80abd0d87b80b78c248643b49aa6d94673b413609b"
],
"markers": "python_version < '3.4'",
"version": "==0.6.0.post1"
},
"cryptography": {
"hashes": [
"sha256:22f8251f68953553af4f9c11ec5f191198bc96cff9f0ac5dd5ff94daede0ee6d",
"sha256:284e275e3c099a80831f9898fb5c9559120d27675c3521278faba54e584a7832",
"sha256:3e17d02941c0f169c5b877597ca8be895fca0e5e3eb882526a74aa4804380a98",
"sha256:52a47e60953679eea0b4d490ca3c241fb1b166a7b161847ef4667dfd49e7699d",
"sha256:57b8c1ed13b8aa386cabbfde3be175d7b155682470b0e259fecfe53850967f8a",
"sha256:6a8f64ed096d13f92d1f601a92d9fd1f1025dc73a2ca1ced46dcf5e0d4930943",
"sha256:6e8a3c7c45101a7eeee93102500e1b08f2307c717ff553fcb3c1127efc9b6917",
"sha256:7ef41304bf978f33cfb6f43ca13bb0faac0c99cda33693aa20ad4f5e34e8cb8f",
"sha256:87c2fffd61e934bc0e2c927c3764c20b22d7f5f7f812ee1a477de4c89b044ca6",
"sha256:88069392cd9a1e68d2cfd5c3a2b0d72a44ef3b24b8977a4f7956e9e3c4c9477a",
"sha256:8a0866891326d3badb17c5fd3e02c926b635e8923fa271b4813cd4d972a57ff3",
"sha256:8f0fd8b0751d75c4483c534b209e39e918f0d14232c0d8a2a76e687f64ced831",
"sha256:9a07e6d255053674506091d63ab4270a119e9fc83462c7ab1dbcb495b76307af",
"sha256:9a8580c9afcdcddabbd064c0a74f337af74ff4529cdf3a12fa2e9782d677a2e5",
"sha256:bd80bc156d3729b38cb227a5a76532aef693b7ac9e395eea8063ee50ceed46a5",
"sha256:d1cbc3426e6150583b22b517ef3720036d7e3152d428c864ff0f3fcad2b97591",
"sha256:e15ac84dcdb89f92424cbaca4b0b34e211e7ce3ee7b0ec0e4f3c55cee65fae5a",
"sha256:e4789b84f8dedf190148441f7c5bfe7244782d9cbb194a36e17b91e7d3e1cca9",
"sha256:f01c9116bfb3ad2831e125a73dcd957d173d6ddca7701528eff1e7d97972872c",
"sha256:f0e3986f6cce007216b23c490f093f35ce2068f3c244051e559f647f6731b7ae",
"sha256:f2aa3f8ba9e2e3fd49bd3de743b976ab192fbf0eb0348cebde5d2a9de0090a9f",
"sha256:fb70a4cedd69dc52396ee114416a3656e011fb0311fca55eb55c7be6ed9c8aef"
],
"index": "pypi",
"version": "==3.2"
},
"distro": {
"hashes": [
"sha256:0e58756ae38fbd8fc3020d54badb8eae17c5b9dcbed388b17bb55b8a5928df92",
"sha256:df74eed763e18d10d0da624258524ae80486432cd17392d9c3d96f5e83cd2799"
],
"version": "==1.5.0"
},
"enum34": {
"hashes": [
"sha256:a98a201d6de3f2ab3db284e70a33b0f896fbf35f8086594e8c9e74b909058d53",
"sha256:c3858660960c984d6ab0ebad691265180da2b43f07e061c0f8dca9ef3cffd328",
"sha256:cce6a7477ed816bd2542d03d53db9f0db935dd013b70f336a95c73979289f248"
],
"markers": "python_version < '3'",
"version": "==1.1.10"
},
"funcsigs": {
"hashes": [
"sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca",
"sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"
],
"markers": "python_version < '3.0'",
"version": "==1.0.2"
},
"fxapom": {
"hashes": [
"sha256:56fdff0a0f0ea58831337e3a859971f98c59fd028ebc14baa8e37ae08a40efa0",
"sha256:5fb902afaaa9d9b82b5d1d54b9e19f1f4c9be128deb3b0e0ac82a9303f76000f"
],
"index": "pypi",
"version": "==1.10.2"
},
"hawkauthlib": {
"hashes": [
"sha256:935878d3a75832aa76f78ddee13491f1466cbd69a8e7e4248902763cf9953ba9",
"sha256:effd64a2572e3c0d9090b55ad2180b36ad50e7760bea225cb6ce2248f421510d"
],
"version": "==2.0.0"
},
"idna": {
"hashes": [
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
],
"version": "==2.10"
},
"importlib-metadata": {
"hashes": [
"sha256:b8de9eff2b35fb037368f28a7df1df4e6436f578fa74423505b6c6a778d5b5dd",
"sha256:c2d6341ff566f609e89a2acb2db190e5e1d23d5409d6cc8d2fe34d72443876d4"
],
"markers": "python_version < '3.8'",
"version": "==2.1.1"
},
"ipaddress": {
"hashes": [
"sha256:6e0f4a39e66cb5bb9a137b00276a2eff74f93b71dcbdad6f10ff7df9d3557fcc",
"sha256:b7f8e0369580bb4a24d5ba1d7cc29660a4a6987763faf1d8a8046830e020e7e2"
],
"markers": "python_version < '3'",
"version": "==1.0.23"
},
"more-itertools": {
"hashes": [
"sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4",
"sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc",
"sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9"
],
"markers": "python_version <= '2.7'",
"version": "==5.0.0"
},
"mozdevice": {
"hashes": [
"sha256:074ba1ff99b18ccc1931538a161be2410d0f9cee122df852b3bc73e1000fbcad",
"sha256:a5a1e882a72df71165f6322def9b5e1d677d39d25f62157f3e0dc554b5ae04dc"
],
"version": "==4.0.3"
},
"mozdownload": {
"hashes": [
"sha256:1664b0bf48eab69fafa73d3fc4dc19f4c66dfc21045fab3ca76a29b3eeb31702",
"sha256:d861936c2efcc7620858a097907bfaba5d6d114867b6633e4301da9263627819"
],
"index": "pypi",
"version": "==1.26.0"
},
"mozfile": {
"hashes": [
"sha256:e5dc835582ea150e35ecd57e9d86cb707d3aa3b2505679db7332326dd49fd6b8"
],
"version": "==2.1.0"
},
"mozinfo": {
"hashes": [
"sha256:4961ebef3c5474b9ca470142f88b5de774a069f4e105663a5152b0ef4659785a"
],
"version": "==1.2.2"
},
"mozinstall": {
"hashes": [
"sha256:219ba7c51308433487b4f30a2615cb9b3ecd40a76b9faf41cf1b1b005bb5dda7",
"sha256:bbc31a18ee8a1fbec74b67b99c6c0289ffc7daf39eb5b5ff7dc99f1be687eb08"
],
"index": "pypi",
"version": "==2.0.0"
},
"mozlog": {
"hashes": [
"sha256:4719d3d00bf1a0b77285d306eb3180f9c1311fffae9640a423fad9d80170e43d",
"sha256:d035f722c15d700e4a7b48b90bdda0a6ad83e25482760949d1abd73468bad07f"
],
"version": "==7.0.1"
},
"mozprocess": {
"hashes": [
"sha256:08e1036b53819fd144331f6dfbbb17fc8ca782bbed2e28b4aa771b8b91f7dffb",
"sha256:54dc59e7f5a9c2c2930bffb7935f36dddd1d94c9fc6ed179e893d2dff353995a"
],
"version": "==1.2.1"
},
"mozprofile": {
"hashes": [
"sha256:5b93462c16ba7c6cd7010035765627d565c2adc7c58ac8bf82a3b1b2c14f0daa",
"sha256:9f77840583432bc5605375b760a6c420328f2dc95c3e8950245e4b01d65da67e"
],
"index": "pypi",
"version": "==2.5.0"
},
"mozrunner": {
"hashes": [
"sha256:5e1bdf1709b4b8cb86b3daf3dbc9352d2abfc8428e26cc75a68ce87a565f4f25",
"sha256:f223e9ca7f0acd3f93d4c30760f8d976d41da81edf686cd5063d2973ebbebcfb"
],
"index": "pypi",
"version": "==7.8.0"
},
"mozterm": {
"hashes": [
"sha256:b1e91acec188de07c704dbb7b0100a7be5c1e06567b3beb67f6ea11d00a483a4",
"sha256:f5eafa25c23d391e2a2bb1dd45ee928fc9e3c811977a3856b5a5a0778011053c"
],
"version": "==1.0.0"
},
"mozversion": {
"hashes": [
"sha256:42f2ce3c23e1835071d1d6f52ebec524d0bfcc036d043cfa854439f6d1dacff0",
"sha256:fe8e90ba54e8172113400ea10ea984827638ddd7c8329ca74426fc55c6886159"
],
"index": "pypi",
"version": "==2.3.0"
},
"packaging": {
"hashes": [
"sha256:05af3bb85d320377db281cf254ab050e1a7ebcbf5410685a9a407e18a1f81236",
"sha256:eb41423378682dadb7166144a4926e443093863024de508ca5c9737d6bc08376"
],
"version": "==20.7"
},
"pathlib2": {
"hashes": [
"sha256:0ec8205a157c80d7acc301c0b18fbd5d44fe655968f5d947b6ecef5290fc35db",
"sha256:6cd9a47b597b37cc57de1c05e56fb1a1c9cc9fab04fe78c29acd090418529868"
],
"markers": "python_version < '3.6'",
"version": "==2.3.5"
},
"pluggy": {
"hashes": [
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
],
"version": "==0.13.1"
},
"progressbar2": {
"hashes": [
"sha256:ef72be284e7f2b61ac0894b44165926f13f5d995b2bf3cd8a8dedc6224b255a7",
"sha256:fe2738e7ecb7df52ad76307fe610c460c52b50f5335fd26c3ab80ff7655ba1e0"
],
"version": "==3.53.1"
},
"py": {
"hashes": [
"sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2",
"sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"
],
"version": "==1.9.0"
},
"pybrowserid": {
"hashes": [
"sha256:6c227669e87cc25796ae76f6a0ef65025528c8ad82d352679fa9a3e5663a71e3",
"sha256:8e237d6a2bc9ead849a4472a84d3e6a9309bec99cf8e10d36213710dda8df8ca"
],
"version": "==0.14.0"
},
"pycparser": {
"hashes": [
"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
"sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
],
"version": "==2.20"
},
"pyfxa": {
"hashes": [
"sha256:6c85cd08cf05f7138dee1cf2a8a1d68fd428b7b5ad488917c70a2a763d651cdb"
],
"version": "==0.7.7"
},
"pyjwt": {
"hashes": [
"sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e",
"sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"
],
"version": "==1.7.1"
},
"pyparsing": {
"hashes": [
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
],
"version": "==2.4.7"
},
"pypom": {
"hashes": [
"sha256:4bdd57fceb72d7e6a3645cf6c9322f490d9cfb5d777eac2c851a3b658b813939",
"sha256:6772ec99f0a21a5bdc8c092007a8c813ed18359e67ed70258bbb233df5e28829"
],
"version": "==2.2.0"
},
"pytest": {
"hashes": [
"sha256:19e8f75eac01dd3f211edd465b39efbcbdc8fc5f7866d7dd49fedb30d8adf339",
"sha256:c77a5f30a90e0ce24db9eaa14ddfd38d4afb5ea159309bdd2dae55b931bc9324"
],
"index": "pypi",
"version": "==4.6.9"
},
"pytest-fxa": {
"hashes": [
"sha256:778dfdb019f1e0af8744704fe5f7ac5c08fd5d45ff054023b0a18d5f99d737f1",
"sha256:b75967e74e9b2f3ffa5558421fdf61c7fff5948fc9d7e357e7147c682988ecc1"
],
"index": "pypi",
"version": "==1.4.0"
},
"pytest-html": {
"hashes": [
"sha256:06e7e13131649b4fe522cf04054efb7b4749ff2c7160755e4acfd8e89a7e5955",
"sha256:f0fae6de71f02f62f9460f628d0c5f70b0cdc86bb393239860c7dec70fd2973d"
],
"index": "pypi",
"version": "==1.22.1"
},
"pytest-metadata": {
"hashes": [
"sha256:2071a59285de40d7541fde1eb9f1ddea1c9db165882df82781367471238b66ba",
"sha256:c29a1fb470424926c63154c1b632c02585f2ba4282932058a71d35295ff8c96d"
],
"index": "pypi",
"version": "==1.8.0"
},
"python-utils": {
"hashes": [
"sha256:ebaadab29d0cb9dca0a82eab9c405f5be5125dbbff35b8f32cc433fa498dbaa7",
"sha256:f21fc09ff58ea5ebd1fd2e8ef7f63e39d456336900f26bdc9334a03a3f7d8089"
],
"version": "==2.4.0"
},
"redo": {
"hashes": [
"sha256:36784bf8ae766e14f9db0e377ccfa02835d648321d2007b6ae0bf4fd612c0f94",
"sha256:71161cb0e928d824092a5f16203939bbc0867ce4c4685db263cf22c3ae7634a8"
],
"version": "==2.0.3"
},
"requests": {
"hashes": [
"sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee",
"sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"
],
"index": "pypi",
"version": "==2.23.0"
},
"scandir": {
"hashes": [
"sha256:2586c94e907d99617887daed6c1d102b5ca28f1085f90446554abf1faf73123e",
"sha256:2ae41f43797ca0c11591c0c35f2f5875fa99f8797cb1a1fd440497ec0ae4b022",
"sha256:2b8e3888b11abb2217a32af0766bc06b65cc4a928d8727828ee68af5a967fa6f",
"sha256:2c712840c2e2ee8dfaf36034080108d30060d759c7b73a01a52251cc8989f11f",
"sha256:4d4631f6062e658e9007ab3149a9b914f3548cb38bfb021c64f39a025ce578ae",
"sha256:67f15b6f83e6507fdc6fca22fedf6ef8b334b399ca27c6b568cbfaa82a364173",
"sha256:7d2d7a06a252764061a020407b997dd036f7bd6a175a5ba2b345f0a357f0b3f4",
"sha256:8c5922863e44ffc00c5c693190648daa6d15e7c1207ed02d6f46a8dcc2869d32",
"sha256:92c85ac42f41ffdc35b6da57ed991575bdbe69db895507af88b9f499b701c188",
"sha256:b24086f2375c4a094a6b51e78b4cf7ca16c721dcee2eddd7aa6494b42d6d519d",
"sha256:cb925555f43060a1745d0a321cca94bcea927c50114b623d73179189a4e100ac"
],
"markers": "python_version < '3.5'",
"version": "==1.10.0"
},
"selenium": {
"hashes": [
"sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c",
"sha256:deaf32b60ad91a4611b98d8002757f29e6f2c2d5fcaf202e1c9ad06d6772300d"
],
"version": "==3.141.0"
},
"six": {
"hashes": [
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
],
"version": "==1.15.0"
},
"treeherder-client": {
"hashes": [
"sha256:4020809424384574277232023c78bcee436ec5474020b4430b4770f0ddd8bba3",
"sha256:db25150480d0501c79b72966899e5c901a5a625e12739389f6bee03273e1d002"
],
"version": "==5.0.0"
},
"urllib3": {
"hashes": [
"sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2",
"sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"
],
"version": "==1.25.11"
},
"wcwidth": {
"hashes": [
"sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784",
"sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"
],
"version": "==0.2.5"
},
"webob": {
"hashes": [
"sha256:a3c89a8e9ba0aeb17382836cdb73c516d0ecf6630ec40ec28288f3ed459ce87b",
"sha256:aa3a917ed752ba3e0b242234b2a373f9c4e2a75d35291dcbe977649bd21fd108"
],
"version": "==1.8.6"
},
"zipp": {
"hashes": [
"sha256:c70410551488251b0fee67b460fb9a536af8d6f9f008ad10ac51f615b6a521b1",
"sha256:e0d9e63797e483a30d27e09fffd308c59a700d365ec34e93cc100844168bf921"
],
"version": "==1.2.0"
},
"zope.component": {
"hashes": [
"sha256:607628e4c84f7887a69a958542b5c304663e726b73aba0882e3a3f059bff14f3",
"sha256:91628918218b3e6f6323de2a7b845e09ddc5cae131c034896c051b084bba3c92"
],
"version": "==4.6.2"
},
"zope.deferredimport": {
"hashes": [
"sha256:57b2345e7b5eef47efcd4f634ff16c93e4265de3dcf325afc7315ade48d909e1",
"sha256:9a0c211df44aa95f1c4e6d2626f90b400f56989180d3ef96032d708da3d23e0a"
],
"version": "==4.3.1"
},
"zope.deprecation": {
"hashes": [
"sha256:0d453338f04bacf91bbfba545d8bcdf529aa829e67b705eac8c1a7fdce66e2df",
"sha256:f1480b74995958b24ce37b0ef04d3663d2683e5d6debc96726eff18acf4ea113"
],
"version": "==4.4.0"
},
"zope.event": {
"hashes": [
"sha256:2666401939cdaa5f4e0c08cf7f20c9b21423b95e88f4675b1443973bdb080c42",
"sha256:5e76517f5b9b119acf37ca8819781db6c16ea433f7e2062c4afc2b6fbedb1330"
],
"version": "==4.5.0"
},
"zope.hookable": {
"hashes": [
"sha256:0194b9b9e7f614abba60c90b231908861036578297515d3d6508eb10190f266d",
"sha256:0c2977473918bdefc6fa8dfb311f154e7f13c6133957fe649704deca79b92093",
"sha256:17b8bdb3b77e03a152ca0d5ca185a7ae0156f5e5a2dbddf538676633a1f7380f",
"sha256:29d07681a78042cdd15b268ae9decffed9ace68a53eebeb61d65ae931d158841",
"sha256:36fb1b35d1150267cb0543a1ddd950c0bc2c75ed0e6e92e3aaa6ac2e29416cb7",
"sha256:3aed60c2bb5e812bbf9295c70f25b17ac37c233f30447a96c67913ba5073642f",
"sha256:3cac1565cc768911e72ca9ec4ddf5c5109e1fef0104f19f06649cf1874943b60",
"sha256:3d4bc0cc4a37c3cd3081063142eeb2125511db3c13f6dc932d899c512690378e",
"sha256:3f73096f27b8c28be53ffb6604f7b570fbbb82f273c6febe5f58119009b59898",
"sha256:522d1153d93f2d48aa0bd9fb778d8d4500be2e4dcf86c3150768f0e3adbbc4ef",
"sha256:523d2928fb7377bbdbc9af9c0b14ad73e6eaf226349f105733bdae27efd15b5a",
"sha256:5848309d4fc5c02150a45e8f8d2227e5bfda386a508bbd3160fed7c633c5a2fa",
"sha256:6781f86e6d54a110980a76e761eb54590630fd2af2a17d7edf02a079d2646c1d",
"sha256:6fd27921ebf3aaa945fa25d790f1f2046204f24dba4946f82f5f0a442577c3e9",
"sha256:70d581862863f6bf9e175e85c9d70c2d7155f53fb04dcdb2f73cf288ca559a53",
"sha256:81867c23b0dc66c8366f351d00923f2bc5902820a24c2534dfd7bf01a5879963",
"sha256:81db29edadcbb740cd2716c95a297893a546ed89db1bfe9110168732d7f0afdd",
"sha256:86bd12624068cea60860a0759af5e2c3adc89c12aef6f71cf12f577e28deefe3",
"sha256:9c184d8f9f7a76e1ced99855ccf390ffdd0ec3765e5cbf7b9cada600accc0a1e",
"sha256:acc789e8c29c13555e43fe4bf9fcd15a65512c9645e97bbaa5602e3201252b02",
"sha256:afaa740206b7660d4cc3b8f120426c85761f51379af7a5b05451f624ad12b0af",
"sha256:b5f5fa323f878bb16eae68ea1ba7f6c0419d4695d0248bed4b18f51d7ce5ab85",
"sha256:bd89e0e2c67bf4ac3aca2a19702b1a37269fb1923827f68324ac2e7afd6e3406",
"sha256:c212de743283ec0735db24ec6ad913758df3af1b7217550ff270038062afd6ae",
"sha256:ca553f524293a0bdea05e7f44c3e685e4b7b022cb37d87bc4a3efa0f86587a8d",
"sha256:cab67065a3db92f636128d3157cc5424a145f82d96fb47159c539132833a6d36",
"sha256:d3b3b3eedfdbf6b02898216e85aa6baf50207f4378a2a6803d6d47650cd37031",
"sha256:d9f4a5a72f40256b686d31c5c0b1fde503172307beb12c1568296e76118e402c",
"sha256:df5067d87aaa111ed5d050e1ee853ba284969497f91806efd42425f5348f1c06",
"sha256:e2587644812c6138f05b8a41594a8337c6790e3baf9a01915e52438c13fc6bef",
"sha256:e27fd877662db94f897f3fd532ef211ca4901eb1a70ba456f15c0866a985464a",
"sha256:e427ebbdd223c72e06ba94c004bb04e996c84dec8a0fa84e837556ae145c439e",
"sha256:e583ad4309c203ef75a09d43434cf9c2b4fa247997ecb0dcad769982c39411c7",
"sha256:e760b2bc8ece9200804f0c2b64d10147ecaf18455a2a90827fbec4c9d84f3ad5",
"sha256:ea9a9cc8bcc70e18023f30fa2f53d11ae069572a162791224e60cd65df55fb69",
"sha256:ecb3f17dce4803c1099bd21742cd126b59817a4e76a6544d31d2cca6e30dbffd",
"sha256:ed794e3b3de42486d30444fb60b5561e724ee8a2d1b17b0c2e0f81e3ddaf7a87",
"sha256:ee885d347279e38226d0a437b6a932f207f691c502ee565aba27a7022f1285df",
"sha256:fd5e7bc5f24f7e3d490698f7b854659a9851da2187414617cd5ed360af7efd63",
"sha256:fe45f6870f7588ac7b2763ff1ce98cce59369717afe70cc353ec5218bc854bcc"
],
"version": "==5.0.1"
},
"zope.interface": {
"hashes": [
"sha256:05a97ba92c1c7c26f25c9f671aa1ef85ffead6cdad13770e5b689cf983adc7e1",
"sha256:07d61722dd7d85547b7c6b0f5486b4338001fab349f2ac5cabc0b7182eb3425d",
"sha256:0a990dcc97806e5980bbb54b2e46b9cde9e48932d8e6984daf71ef1745516123",
"sha256:150e8bcb7253a34a4535aeea3de36c0bb3b1a6a47a183a95d65a194b3e07f232",
"sha256:1743bcfe45af8846b775086471c28258f4c6e9ee8ef37484de4495f15a98b549",
"sha256:1b5f6c8fff4ed32aa2dd43e84061bc8346f32d3ba6ad6e58f088fe109608f102",
"sha256:21e49123f375703cf824214939d39df0af62c47d122d955b2a8d9153ea08cfd5",
"sha256:21f579134a47083ffb5ddd1307f0405c91aa8b61ad4be6fd5af0171474fe0c45",
"sha256:27c267dc38a0f0079e96a2945ee65786d38ef111e413c702fbaaacbab6361d00",
"sha256:299bde0ab9e5c4a92f01a152b7fbabb460f31343f1416f9b7b983167ab1e33bc",
"sha256:2ab88d8f228f803fcb8cb7d222c579d13dab2d3622c51e8cf321280da01102a7",
"sha256:2ced4c35061eea623bc84c7711eedce8ecc3c2c51cd9c6afa6290df3bae9e104",
"sha256:2dcab01c660983ba5e5a612e0c935141ccbee67d2e2e14b833e01c2354bd8034",
"sha256:32546af61a9a9b141ca38d971aa6eb9800450fa6620ce6323cc30eec447861f3",
"sha256:32b40a4c46d199827d79c86bb8cb88b1bbb764f127876f2cb6f3a47f63dbada3",
"sha256:3cc94c69f6bd48ed86e8e24f358cb75095c8129827df1298518ab860115269a4",
"sha256:42b278ac0989d6f5cf58d7e0828ea6b5951464e3cf2ff229dd09a96cb6ba0c86",
"sha256:495b63fd0302f282ee6c1e6ea0f1c12cb3d1a49c8292d27287f01845ff252a96",
"sha256:4af87cdc0d4b14e600e6d3d09793dce3b7171348a094ba818e2a68ae7ee67546",
"sha256:4b94df9f2fdde7b9314321bab8448e6ad5a23b80542dcab53e329527d4099dcb",
"sha256:4c48ddb63e2b20fba4c6a2bf81b4d49e99b6d4587fb67a6cd33a2c1f003af3e3",
"sha256:4df9afd17bd5477e9f8c8b6bb8507e18dd0f8b4efe73bb99729ff203279e9e3b",
"sha256:518950fe6a5d56f94ba125107895f938a4f34f704c658986eae8255edb41163b",
"sha256:538298e4e113ccb8b41658d5a4b605bebe75e46a30ceca22a5a289cf02c80bec",
"sha256:55465121e72e208a7b69b53de791402affe6165083b2ea71b892728bd19ba9ae",
"sha256:588384d70a0f19b47409cfdb10e0c27c20e4293b74fc891df3d8eb47782b8b3e",
"sha256:6278c080d4afffc9016e14325f8734456831124e8c12caa754fd544435c08386",
"sha256:64ea6c221aeee4796860405e1aedec63424cda4202a7ad27a5066876db5b0fd2",
"sha256:681dbb33e2b40262b33fd383bae63c36d33fd79fa1a8e4092945430744ffd34a",
"sha256:6936aa9da390402d646a32a6a38d5409c2d2afb2950f045a7d02ab25a4e7d08d",
"sha256:778d0ec38bbd288b150a3ae363c8ffd88d2207a756842495e9bffd8a8afbc89a",
"sha256:8251f06a77985a2729a8bdbefbae79ee78567dddc3acbd499b87e705ca59fe24",
"sha256:83b4aa5344cce005a9cff5d0321b2e318e871cc1dfc793b66c32dd4f59e9770d",
"sha256:844fad925ac5c2ad4faaceb3b2520ad016b5280105c6e16e79838cf951903a7b",
"sha256:8ceb3667dd13b8133f2e4d637b5b00f240f066448e2aa89a41f4c2d78a26ce50",
"sha256:92dc0fb79675882d0b6138be4bf0cec7ea7c7eede60aaca78303d8e8dbdaa523",
"sha256:9789bd945e9f5bd026ed3f5b453d640befb8b1fc33a779c1fe8d3eb21fe3fb4a",
"sha256:a2b6d6eb693bc2fc6c484f2e5d93bd0b0da803fa77bf974f160533e555e4d095",
"sha256:aab9f1e34d810feb00bf841993552b8fcc6ae71d473c505381627143d0018a6a",
"sha256:abb61afd84f23099ac6099d804cdba9bd3b902aaaded3ffff47e490b0a495520",
"sha256:adf9ee115ae8ff8b6da4b854b4152f253b390ba64407a22d75456fe07dcbda65",
"sha256:aedc6c672b351afe6dfe17ff83ee5e7eb6ed44718f879a9328a68bdb20b57e11",
"sha256:b7a00ecb1434f8183395fac5366a21ee73d14900082ca37cf74993cf46baa56c",
"sha256:ba32f4a91c1cb7314c429b03afbf87b1fff4fb1c8db32260e7310104bd77f0c7",
"sha256:cbd0f2cbd8689861209cd89141371d3a22a11613304d1f0736492590aa0ab332",
"sha256:e4bc372b953bf6cec65a8d48482ba574f6e051621d157cf224227dbb55486b1e",
"sha256:eccac3d9aadc68e994b6d228cb0c8919fc47a5350d85a1b4d3d81d1e98baf40c",
"sha256:efd550b3da28195746bb43bd1d815058181a7ca6d9d6aa89dd37f5eefe2cacb7",
"sha256:efef581c8ba4d990770875e1a2218e856849d32ada2680e53aebc5d154a17e20",
"sha256:f057897711a630a0b7a6a03f1acf379b6ba25d37dc5dc217a97191984ba7f2fc",
"sha256:f37d45fab14ffef9d33a0dc3bc59ce0c5313e2253323312d47739192da94f5fd",
"sha256:f44906f70205d456d503105023041f1e63aece7623b31c390a0103db4de17537"
],
"version": "==5.2.0"
},
"zope.proxy": {
"hashes": [
"sha256:00573dfa755d0703ab84bb23cb6ecf97bb683c34b340d4df76651f97b0bab068",
"sha256:092049280f2848d2ba1b57b71fe04881762a220a97b65288bcb0968bb199ec30",
"sha256:0cbd27b4d3718b5ec74fc65ffa53c78d34c65c6fd9411b8352d2a4f855220cf1",
"sha256:17fc7e16d0c81f833a138818a30f366696653d521febc8e892858041c4d88785",
"sha256:19577dfeb70e8a67249ba92c8ad20589a1a2d86a8d693647fa8385408a4c17b0",
"sha256:207aa914576b1181597a1516e1b90599dc690c095343ae281b0772e44945e6a4",
"sha256:219a7db5ed53e523eb4a4769f13105118b6d5b04ed169a283c9775af221e231f",
"sha256:2b50ea79849e46b5f4f2b0247a3687505d32d161eeb16a75f6f7e6cd81936e43",
"sha256:5903d38362b6c716e66bbe470f190579c530a5baf03dbc8500e5c2357aa569a5",
"sha256:5c24903675e271bd688c6e9e7df5775ac6b168feb87dbe0e4bcc90805f21b28f",
"sha256:5ef6bc5ed98139e084f4e91100f2b098a0cd3493d4e76f9d6b3f7b95d7ad0f06",
"sha256:61b55ae3c23a126a788b33ffb18f37d6668e79a05e756588d9e4d4be7246ab1c",
"sha256:63ddb992931a5e616c87d3d89f5a58db086e617548005c7f9059fac68c03a5cc",
"sha256:6943da9c09870490dcfd50c4909c0cc19f434fa6948f61282dc9cb07bcf08160",
"sha256:6ad40f85c1207803d581d5d75e9ea25327cd524925699a83dfc03bf8e4ba72b7",
"sha256:6b44433a79bdd7af0e3337bd7bbcf53dd1f9b0fa66bf21bcb756060ce32a96c1",
"sha256:6bbaa245015d933a4172395baad7874373f162955d73612f0b66b6c2c33b6366",
"sha256:7007227f4ea85b40a2f5e5a244479f6a6dfcf906db9b55e812a814a8f0e2c28d",
"sha256:74884a0aec1f1609190ec8b34b5d58fb3b5353cf22b96161e13e0e835f13518f",
"sha256:7d25fe5571ddb16369054f54cdd883f23de9941476d97f2b92eb6d7d83afe22d",
"sha256:7e162bdc5e3baad26b2262240be7d2bab36991d85a6a556e48b9dfb402370261",
"sha256:814d62678dc3a30f4aa081982d830b7c342cf230ffc9d030b020cb154eeebf9e",
"sha256:8878a34c5313ee52e20aa50b03138af8d472bae465710fb954d133a9bfd3c38d",
"sha256:a66a0d94e5b081d5d695e66d6667e91e74d79e273eee95c1747717ba9cb70792",
"sha256:a69f5cbf4addcfdf03dda564a671040127a6b7c34cf9fe4973582e68441b63fa",
"sha256:b00f9f0c334d07709d3f73a7cb8ae63c6ca1a90c790a63b5e7effa666ef96021",
"sha256:b6ed71e4a7b4690447b626f499d978aa13197a0e592950e5d7020308f6054698",
"sha256:bdf5041e5851526e885af579d2f455348dba68d74f14a32781933569a327fddf",
"sha256:be034360dd34e62608419f86e799c97d389c10a0e677a25f236a971b2f40dac9",
"sha256:cc8f590a5eed30b314ae6b0232d925519ade433f663de79cc3783e4b10d662ba",
"sha256:cd7a318a15fe6cc4584bf3c4426f092ed08c0fd012cf2a9173114234fe193e11",
"sha256:cf19b5f63a59c20306e034e691402b02055c8f4e38bf6792c23cad489162a642",
"sha256:cfc781ce442ec407c841e9aa51d0e1024f72b6ec34caa8fdb6ef9576d549acf2",
"sha256:dea9f6f8633571e18bc20cad83603072e697103a567f4b0738d52dd0211b4527",
"sha256:e4a86a1d5eb2cce83c5972b3930c7c1eac81ab3508464345e2b8e54f119d5505",
"sha256:e7106374d4a74ed9ff00c46cc00f0a9f06a0775f8868e423f85d4464d2333679",
"sha256:e98a8a585b5668aa9e34d10f7785abf9545fe72663b4bfc16c99a115185ae6a5",
"sha256:f64840e68483316eb58d82c376ad3585ca995e69e33b230436de0cdddf7363f9",
"sha256:f8f4b0a9e6683e43889852130595c8854d8ae237f2324a053cdd884de936aa9b",
"sha256:fc45a53219ed30a7f670a6d8c98527af0020e6fd4ee4c0a8fb59f147f06d816c"
],
"version": "==4.3.5"
}
},
"develop": {}
}

View File

@ -0,0 +1,255 @@
/* 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 net.waterfox.android.syncintegration
import android.os.SystemClock.sleep
import android.widget.EditText
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import okhttp3.mockwebserver.MockWebServer
import org.hamcrest.Matchers.allOf
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import net.waterfox.android.R
import net.waterfox.android.helpers.AndroidAssetDispatcher
import net.waterfox.android.helpers.HomeActivityTestRule
import net.waterfox.android.helpers.TestAssetHelper
import net.waterfox.android.helpers.ext.toUri
import net.waterfox.android.helpers.ext.waitNotNull
import net.waterfox.android.ui.robots.accountSettings
import net.waterfox.android.ui.robots.homeScreen
import net.waterfox.android.ui.robots.navigationToolbar
import net.waterfox.android.ui.robots.settingsSubMenuLoginsAndPassword
@Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
class SyncIntegrationTest {
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer
@get:Rule
val activityTestRule = HomeActivityTestRule()
@Before
fun setUp() {
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
// History item Desktop -> Waterfox
@Test
fun checkHistoryFromDesktopTest() {
signInFxSync()
tapReturnToPreviousApp()
// Let's wait until homescreen is shown to go to three dot menu
TestAssetHelper.waitingTime
mDevice.waitNotNull(Until.findObjects(By.res("net.waterfox.android.debug:id/counter_root")))
homeScreen {
}.openThreeDotMenu {
}.openHistory {
}
historyAfterSyncIsShown()
}
// Bookmark item Desktop -> Waterfox
@Test
fun checkBookmarkFromDesktopTest() {
signInFxSync()
tapReturnToPreviousApp()
homeScreen {
}.openThreeDotMenu {
}.openBookmarks { }
bookmarkAfterSyncIsShown()
}
@Test
fun checkAccountSettings() {
signInFxSync()
mDevice.waitNotNull(Until.findObjects(By.text("Account")), TestAssetHelper.waitingTime)
goToAccountSettings()
// This function to be added to the robot once the status of checkboxes can be checked
// currently is not possible to select each one (History/Bookmark) and verify its status
// verifyCheckBoxesSelected()
// Then select/unselect each one and verify again that its status is correct
// See issue #6544
accountSettings {
verifyBookmarksCheckbox()
verifyHistoryCheckbox()
verifySignOutButton()
verifyDeviceName()
}.disconnectAccount {
mDevice.waitNotNull(Until.findObjects(By.text("Settings")), TestAssetHelper.waitingTime)
verifySettingsView()
}
}
// Login item Desktop -> Waterfox
@Test
fun checkLoginsFromDesktopTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openLoginsAndPasswordSubMenu {
}.openSyncLogins {
// Tap to sign in from Logins menu
tapOnUseEmailToSignIn()
typeEmail()
tapOnContinueButton()
typePassword()
tapOnSignIn()
}
// Automatically goes back to Logins and passwords view
settingsSubMenuLoginsAndPassword {
verifyDefaultView()
// Sync logings option is set to Off, no synced logins yet
verifyDefaultViewBeforeSyncComplete()
}.openSavedLogins {
// Discard the secure your device message
tapSetupLater()
// Check the logins synced
verifySavedLoginsAfterSync()
}.goBack {
// After checking the synced logins
// on Logins and Passwords menu the Sync logins option is set to On
verifyDefaultViewAfterSync()
}
}
// Bookmark item Waterfox -> Desktop
@Test
fun checkBookmarkFromDeviceTest() {
val defaultWebPage = "example.com".toUri()!!
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage) {
}.openThreeDotMenu {
}.bookmarkPage {
}.openThreeDotMenu {
}.openSettings {
}.openTurnOnSyncMenu {
useEmailInsteadButton()
typeEmail()
tapOnContinueButton()
typePassword()
sleep(TestAssetHelper.waitingTimeShort)
tapOnSignIn()
}
}
// History item Waterfox -> Desktop
@Test
fun checkHistoryFromDeviceTest() {
val defaultWebPage = "example.com".toUri()!!
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage) {
}.openThreeDotMenu {
}.openSettings {
}.openTurnOnSyncMenu {
useEmailInsteadButton()
typeEmail()
tapOnContinueButton()
typePassword()
sleep(TestAssetHelper.waitingTimeShort)
tapOnSignIn()
}
}
// Useful functions for the tests
fun typeEmail() {
val emailInput = mDevice.findObject(
UiSelector()
.instance(0)
.className(EditText::class.java)
)
emailInput.waitForExists(TestAssetHelper.waitingTime)
val emailAddress = javaClass.classLoader!!.getResource("email.txt").readText()
emailInput.setText(emailAddress)
}
fun tapOnContinueButton() {
val continueButton = mDevice.findObject(By.res("submit-btn"))
continueButton.clickAndWait(Until.newWindow(), TestAssetHelper.waitingTime)
}
fun typePassword() {
val passwordInput = mDevice.findObject(
UiSelector()
.instance(0)
.className(EditText::class.java)
)
val passwordValue = javaClass.classLoader!!.getResource("password.txt").readText()
passwordInput.setText(passwordValue)
}
fun tapOnSignIn() {
mDevice.waitNotNull(Until.findObjects(By.text("Sign in")))
// Let's tap on enter, sometimes depending on the device the sign in button is
// hidden by the keyboard
mDevice.pressEnter()
}
fun historyAfterSyncIsShown() {
mDevice.waitNotNull(Until.findObjects(By.text("http://www.example.com/")), TestAssetHelper.waitingTime)
}
fun bookmarkAfterSyncIsShown() {
val bookmarkEntry = mDevice.findObject(By.text("Example Domain"))
bookmarkEntry.isEnabled()
}
fun tapReturnToPreviousApp() {
mDevice.waitNotNull(Until.findObjects(By.text("Save")), TestAssetHelper.waitingTime)
mDevice.waitNotNull(Until.findObjects(By.text("Settings")), TestAssetHelper.waitingTime)
/* Wait until the Settings shows the account synced */
mDevice.waitNotNull(Until.findObjects(By.text("Account")), TestAssetHelper.waitingTime)
mDevice.waitNotNull(Until.findObjects(By.res("net.waterfox.android.debug:id/email")), TestAssetHelper.waitingTime)
TestAssetHelper.waitingTime
// Go to Homescreen
mDevice.pressBack()
}
fun signInFxSync() {
homeScreen {
}.openThreeDotMenu {
verifySettingsButton()
}.openSettings {}
settingsAccount()
useEmailInsteadButton()
typeEmail()
tapOnContinueButton()
typePassword()
sleep(TestAssetHelper.waitingTimeShort)
tapOnSignIn()
}
fun goToAccountSettings() {
enterAccountSettings()
mDevice.waitNotNull(Until.findObjects(By.text("Device name")), TestAssetHelper.waitingTime)
}
}
fun settingsAccount() = onView(allOf(withText("Turn on Sync"))).perform(click())
fun useEmailInsteadButton() = onView(withId(R.id.signInEmailButton)).perform(click())
fun enterAccountSettings() = onView(withId(R.id.email)).perform(click())

View File

@ -0,0 +1,16 @@
import logging
import subprocess
import os
logging.getLogger(__name__).addHandler(logging.NullHandler())
class ADBrun(object):
binary = 'adbrun'
logger = logging.getLogger()
def launch(self):
# First close sim if any then launch
os.system('~/Library/Android/sdk/platform-tools/adb devices | grep emulator | cut -f1 | while read line; do ~/Library/Android/sdk/platform-tools/adb -s $line emu kill; done')
# Then launch sim
os.system("sh launchSimScript.sh")

View File

@ -0,0 +1,180 @@
import io
import json
import os
import time
import os.path as path
from mozdownload import DirectScraper, FactoryScraper
from mozprofile import Profile
import mozinstall
import mozversion
import pytest
import requests
from tps import TPS
from gradlewbuild import GradlewBuild
here = os.path.dirname(__file__)
@pytest.fixture(scope='session')
def firefox(pytestconfig, tmpdir_factory):
binary = os.getenv('MOZREGRESSION_BINARY',
pytestconfig.getoption('firefox'))
if binary is None:
cache_dir = str(pytestconfig.cache.makedir('firefox'))
scraper = FactoryScraper('daily', destination=cache_dir)
build_path = scraper.download()
install_path = str(tmpdir_factory.mktemp('firefox'))
install_dir = mozinstall.install(src=build_path, dest=install_path)
binary = mozinstall.get_binary(install_dir, 'firefox')
version = mozversion.get_version(binary)
if hasattr(pytestconfig, '_metadata'):
pytestconfig._metadata.update(version)
return binary
@pytest.fixture
def firefox_log(pytestconfig, tmpdir):
firefox_log = str(tmpdir.join('firefox.log'))
pytestconfig._firefox_log = firefox_log
yield firefox_log
@pytest.fixture(scope='session')
def tps_addon(pytestconfig, tmpdir_factory):
path = pytestconfig.getoption('tps')
if path is not None:
return path
task_url = 'https://firefox-ci-tc.services.mozilla.com/api/index/v1/task/' \
'gecko.v2.mozilla-central.latest.firefox.addons.tps'
task_id = requests.get(task_url).json().get('taskId')
cache_dir = str(pytestconfig.cache.makedir('tps-{}'.format(task_id)))
addon_url = 'https://firefox-ci-tc.services.mozilla.com/api/queue/v1/task/' \
'{}/artifacts/public/tps.xpi'.format(task_id)
scraper = DirectScraper(addon_url, destination=cache_dir)
return scraper.download()
@pytest.fixture
def tps_config(fxa_account, monkeypatch):
monkeypatch.setenv('FXA_EMAIL', fxa_account.email)
monkeypatch.setenv('FXA_PASSWORD', fxa_account.password)
# Go to resources folder
os.chdir('../../../../..')
resources = r'resources'
resourcesDir = os.path.join(os.getcwd(), resources)
with open (os.path.join(resourcesDir, 'email.txt'), "w") as f:
f.write(fxa_account.email)
with open (os.path.join(resourcesDir, 'password.txt'), "w") as f:
f.write(fxa_account.password)
# Set the path where tests are
os.chdir('../')
currentDir = os.getcwd()
testsDir = currentDir + "/androidTest/java/net/waterfox/android/syncintegration"
os.chdir(testsDir)
yield {'fx_account': {
'username': fxa_account.email,
'password': fxa_account.password}
}
@pytest.fixture
def tps_log(pytestconfig, tmpdir):
tps_log = str(tmpdir.join('tps.log'))
pytestconfig._tps_log = tps_log
yield tps_log
@pytest.fixture
def tps_profile(pytestconfig, tps_addon, tps_config, tps_log, fxa_urls):
preferences = {
'app.update.enabled': False,
'browser.dom.window.dump.enabled': True,
'browser.onboarding.enabled': False,
'browser.sessionstore.resume_from_crash': False,
'browser.shell.checkDefaultBrowser': False,
'browser.startup.homepage_override.mstone': 'ignore',
'browser.startup.page': 0,
'browser.tabs.warnOnClose': False,
'browser.warnOnQuit': False,
'datareporting.policy.dataSubmissionEnabled': False,
# 'devtools.chrome.enabled': True,
# 'devtools.debugger.remote-enabled': True,
'engine.bookmarks.repair.enabled': False,
'extensions.autoDisableScopes': 10,
'extensions.experiments.enabled': True,
'extensions.update.enabled': False,
'extensions.update.notifyUser': False,
# While this line is commented prod is launched instead of stage
'identity.fxaccounts.autoconfig.uri': fxa_urls['content'],
'testing.tps.skipPingValidation': True,
'services.sync.firstSync': 'notReady',
'services.sync.lastversion': '1.0',
'services.sync.log.appender.console': 'Trace',
'services.sync.log.appender.dump': 'Trace',
'services.sync.log.appender.file.level': 'Trace',
'services.sync.log.appender.file.logOnSuccess': True,
'services.sync.log.logger': 'Trace',
'services.sync.log.logger.engine': 'Trace',
'services.sync.testing.tps': True,
'testing.tps.logFile': tps_log,
'toolkit.startup.max_resumed_crashes': -1,
'tps.config': json.dumps(tps_config),
'tps.seconds_since_epoch': int(time.time()),
'xpinstall.signatures.required': False
}
profile = Profile(addons=[tps_addon], preferences=preferences)
pytestconfig._profile = profile.profile
yield profile
@pytest.fixture
def tps(firefox, firefox_log, monkeypatch, pytestconfig, tps_log, tps_profile):
yield TPS(firefox, firefox_log, tps_log, tps_profile)
@pytest.fixture
def gradlewbuild_log(pytestconfig, tmpdir):
gradlewbuild_log = str(tmpdir.join('gradlewbuild.log'))
pytestconfig._gradlewbuild_log = gradlewbuild_log
yield gradlewbuild_log
@pytest.fixture
def gradlewbuild(fxa_account, monkeypatch, gradlewbuild_log):
monkeypatch.setenv('FXA_EMAIL', fxa_account.email)
monkeypatch.setenv('FXA_PASSWORD', fxa_account.password)
yield GradlewBuild(gradlewbuild_log)
def pytest_addoption(parser):
parser.addoption('--firefox', help='path to firefox binary (defaults to '
'downloading latest nightly build)')
parser.addoption('--tps', help='path to tps add-on (defaults to '
'downloading latest nightly build)')
@pytest.mark.hookwrapper
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
extra = getattr(report, 'extra', [])
pytest_html = item.config.pluginmanager.getplugin('html')
profile = getattr(item.config, '_profile', None)
if profile is not None and os.path.exists(profile):
# add sync logs to HTML report
for root, _, files in os.walk(os.path.join(profile, 'weave', 'logs')):
for f in files:
path = os.path.join(root, f)
if pytest_html is not None:
with io.open(path, 'r', encoding='utf8') as f:
extra.append(pytest_html.extras.text(f.read(), 'Sync'))
report.sections.append(('Sync', 'Log: {}'.format(path)))
for log in ('Firefox', 'TPS', 'GradlewBuild'):
attr = '_{}_log'.format(log.lower())
path = getattr(item.config, attr, None)
if path is not None and os.path.exists(path):
if pytest_html is not None:
with io.open(path, 'r', encoding='utf8') as f:
extra.append(pytest_html.extras.text(f.read(), log))
report.sections.append((log, 'Log: {}'.format(path)))
report.extra = extra

View File

@ -0,0 +1,43 @@
import logging
import os
import subprocess
from adbrun import ADBrun
here = os.path.dirname(__file__)
logging.getLogger(__name__).addHandler(logging.NullHandler())
class GradlewBuild(object):
binary = './gradlew'
logger = logging.getLogger()
adbrun = ADBrun()
def __init__(self, log):
self.log = log
def test(self, identifier):
self.adbrun.launch()
# Change path accordingly to go to root folder to run gradlew
os.chdir('../../../../../../../..')
cmd = './gradlew ' + 'app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=net.waterfox.android.syncintegration.SyncIntegrationTest#{}'.format(identifier)
self.logger.info('Running cmd: {}'.format(cmd))
out = ""
try:
out = subprocess.check_output(
cmd,
shell=True,
stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
out = e.output
raise
finally:
# Set the path correctly
testsPath = "app/src/androidTest/java/net/waterfox/android/syncintegration/"
os.chdir(testsPath)
with open(self.log, 'w') as f:
f.write(out)

View File

@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -e
echo "Waiting emulator is ready..."
~/Library/Android/sdk/emulator/emulator -avd Pixel_3_API_28 -wipe-data -no-boot-anim -screen no-touch &
bootanim=""
failcounter=0
timeout_in_sec=360
until [[ "$bootanim" =~ "stopped" ]]; do
bootanim=`~/Library/Android/sdk/platform-tools/adb -e shell getprop init.svc.bootanim 2>&1 &`
if [[ "$bootanim" =~ "device not found" || "$bootanim" =~ "device offline"
|| "$bootanim" =~ "running" ]]; then
let "failcounter += 1"
echo "Waiting for emulator to start"
if [[ $failcounter -gt timeout_in_sec ]]; then
echo "Timeout ($timeout_in_sec seconds) reached; failed to start emulator"
exit 1
fi
fi
sleep 1
done
echo "Emulator is ready"
sleep 10

View File

@ -0,0 +1,4 @@
[pytest]
addopts = --verbose --html=results/index.html
log_cli = true
log_cli_level = info

View File

@ -0,0 +1,26 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* The list of phases mapped to their corresponding profiles. The object
* here must be in strict JSON format, as it will get parsed by the Python
* testrunner (no single quotes, extra comma's, etc).
*/
EnableEngines(["bookmarks"]);
var phases = { "phase1": "profile1" };
// expected bookmark state
var bookmarksCreated = {
"mobile": [{
uri: "http://www.example.com/",
title: "Example Domain"}]
};
// sync and verify bookmarks
Phase("phase1", [
[Sync],
[Bookmarks.add, bookmarksCreated],
[Sync]
]);

View File

@ -0,0 +1,25 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* The list of phases mapped to their corresponding profiles. The object
* here must be in strict JSON format, as it will get parsed by the Python
* testrunner (no single quotes, extra comma's, etc).
*/
EnableEngines(["bookmarks"]);
var phases = { "phase1": "profile1" };
// expected bookmark state
var bookmarksExpected = {
"mobile": [{
uri: "http://www.example.com/",
title: "Example Domain"}]
};
// sync and verify bookmarks
Phase("phase1", [
[Sync],
[Bookmarks.verify, bookmarksExpected],
]);

View File

@ -0,0 +1,33 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* The list of phases mapped to their corresponding profiles. The object
* here must be in strict JSON format, as it will get parsed by the Python
* testrunner (no single quotes, extra comma's, etc).
*/
EnableEngines(["history"]);
var phases = { "phase1": "profile1" };
// expected history state
var historyCreated = [
{ uri: "http://www.example.com/",
visits: [
{ type: 1 ,
date: 0
},
{ type: 2,
date: -1
}
]
}
];
// sync and verify history
Phase("phase1", [
[Sync],
[History.add, historyCreated],
[Sync]
]);

View File

@ -0,0 +1,28 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* The list of phases mapped to their corresponding profiles. The object
* here must be in strict JSON format, as it will get parsed by the Python
* testrunner (no single quotes, extra comma's, etc).
*/
EnableEngines(["history"]);
var phases = { "phase1": "profile1" };
// expected history state
var historyExpected = [
{ uri: "http://www.example.com/",
visits: [
{ type: 1 },
{ type: 2 }
]
}
];
// sync and verify history
Phase("phase1", [
[Sync],
[History.verify, historyExpected]
]);

View File

@ -0,0 +1,26 @@
import os
import sys
def test_sync_account_settings(tps, gradlewbuild):
gradlewbuild.test('checkAccountSettings')
def test_sync_history_from_desktop(tps, gradlewbuild):
tps.run('test_history.js')
gradlewbuild.test('checkHistoryFromDesktopTest')
'''
def test_sync_bookmark_from_desktop(tps, gradlewbuild):
tps.run('test_bookmark.js')
gradlewbuild.test('checkBookmarkFromDesktopTest')
def test_sync_logins_from_desktop(tps, gradlewbuild):
tps.run('test_logins.js')
gradlewbuild.test('checkLoginsFromDesktopTest')
def test_sync_bookmark_from_device(tps, gradlewbuild):
gradlewbuild.test('checkBookmarkFromDeviceTest')
tps.run('test_bookmark_desktop.js')
def test_sync_history_from_device(tps, gradlewbuild):
gradlewbuild.test('checkHistoryFromDeviceTest')
tps.run('test_history_desktop.js')
'''

View File

@ -0,0 +1,30 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* The list of phases mapped to their corresponding profiles. The object
* here must be in strict JSON format, as it will get parsed by the Python
* testrunner (no single quotes, extra comma's, etc).
*/
EnableEngines(["passwords"]);
var phases = { "phase1": "profile1" };
// expected tabs state
var password_list = [{
hostname: "https://accounts.google.com",
submitURL: "https://accounts.google.com/signin/challenge/sl/password",
realm: null,
username: "iosmztest",
password: "test15mz",
usernameField: "Email",
passwordField: "Passwd",
}];
// sync and verify tabs
Phase("phase1", [
[Sync],
[Passwords.add, password_list],
[Sync]
]);

View File

@ -0,0 +1,52 @@
import logging
import os
from mozrunner import FirefoxRunner
logging.getLogger(__name__).addHandler(logging.NullHandler())
TIMEOUT = 60
class TPS(object):
logger = logging.getLogger()
def __init__(self, firefox, firefox_log, tps_log, profile):
self.firefox = firefox
self.firefox_log = open(firefox_log, 'w')
self.tps_log = tps_log
self.profile = profile
def _log(self, line):
self.firefox_log.write(line + '\n')
def run(self, test, phase='phase1', ignore_unused_engines=True):
self.profile.set_preferences({
'testing.tps.testFile': os.path.abspath(test),
'testing.tps.testPhase': phase,
'testing.tps.ignoreUnusedEngines': ignore_unused_engines,
})
args = ['-marionette']
process_args = {'processOutputLine': [self._log]}
self.logger.info('Running: {} {}'.format(self.firefox, ' '.join(args)))
self.logger.info('Using profile at: {}'.format(self.profile.profile))
runner = FirefoxRunner(
binary=self.firefox,
cmdargs=args,
profile=self.profile,
process_args=process_args)
runner.start(timeout=TIMEOUT)
runner.wait(timeout=TIMEOUT)
self.firefox_log.close()
with open(self.tps_log) as f:
for line in f.readlines():
if 'CROSSWEAVE ERROR: ' in line:
raise TPSError(line.partition('CROSSWEAVE ERROR: ')[-1])
with open(self.tps_log) as f:
assert 'test phase {}: PASS'.format(phase) in f.read()
class TPSError(Exception):
pass

View File

@ -0,0 +1,616 @@
/* 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 net.waterfox.android.ui
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import kotlinx.coroutines.runBlocking
import mozilla.appservices.places.BookmarkRoot
import net.waterfox.android.customannotations.SmokeTest
import net.waterfox.android.ext.bookmarkStorage
import net.waterfox.android.ext.settings
import net.waterfox.android.helpers.*
import net.waterfox.android.ui.robots.*
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
/**
* Tests for verifying basic functionality of bookmarks
*/
class BookmarksTest {
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
private lateinit var mockWebServer: MockWebServer
private lateinit var mDevice: UiDevice
private val bookmarksFolderName = "New Folder"
private val testBookmark = object {
var title: String = "Bookmark title"
var url: String = "https://www.test.com/"
}
@get:Rule(order = 0)
val composeTestRule = AndroidComposeTestRule(
HomeActivityIntentTestRule()
) { it.activity }
private val activity by lazy { composeTestRule.activity }
@Rule(order = 1)
@JvmField
val retryTestRule = RetryTestRule(3)
@Before
fun setUp() {
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
val settings = activity.settings()
settings.shouldShowJumpBackInCFR = false
settings.shouldShowTotalCookieProtectionCFR = false
}
@After
fun tearDown() {
mockWebServer.shutdown()
// Clearing all bookmarks data after each test to avoid overlapping data
val bookmarksStorage = activity.bookmarkStorage
runBlocking {
val bookmarks = bookmarksStorage.getTree(BookmarkRoot.Mobile.id)?.children
bookmarks?.forEach { bookmarksStorage.deleteNode(it.guid) }
}
}
@Test
fun verifyEmptyBookmarksMenuTest() {
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
verifyBookmarksMenuView()
verifyAddFolderButton()
verifyCloseButton()
verifyBookmarkTitle("Desktop Bookmarks", composeTestRule)
}
}
@Test
fun verifyEmptyBookmarksListTest() {
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
clickAddFolderButton()
verifyKeyboardVisible()
addNewFolderName("Empty", composeTestRule)
saveNewFolder()
selectFolder("Empty", composeTestRule)
verifyEmptyFolder(composeTestRule)
}
}
@Test
fun defaultDesktopBookmarksFoldersTest() {
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
selectFolder("Desktop Bookmarks", composeTestRule)
verifyFolderTitle("Bookmarks Menu", composeTestRule)
verifyFolderTitle("Bookmarks Toolbar", composeTestRule)
verifyFolderTitle("Other Bookmarks", composeTestRule)
verifySignInToSyncButton(composeTestRule)
}.clickSingInToSyncButton(composeTestRule) {
verifyTurnOnSyncToolbarTitle()
}
}
@Test
fun verifyBookmarkButtonTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
}.bookmarkPage {
}.openThreeDotMenu {
verifyEditBookmarkButton()
}
}
@Test
fun addBookmarkTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
verifyBookmarkedUrl(defaultWebPage.url.toString(), composeTestRule)
verifyBookmarkFavicon(defaultWebPage.url, composeTestRule)
}
}
@Test
fun createBookmarkFolderTest() {
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
clickAddFolderButton()
verifyKeyboardVisible()
addNewFolderName(bookmarksFolderName, composeTestRule)
saveNewFolder()
verifyFolderTitle(bookmarksFolderName, composeTestRule)
verifyKeyboardHidden()
}
}
@Test
fun addBookmarkThenCreateFolderTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
clickAddFolderButton()
verifyKeyboardVisible()
addNewFolderName(bookmarksFolderName, composeTestRule)
saveNewFolder()
verifyBookmarkItemPosition("Desktop Bookmarks", 0, composeTestRule)
verifyBookmarkItemPosition(bookmarksFolderName, 1, composeTestRule)
verifyBookmarkItemPosition(defaultWebPage.title, 2, composeTestRule)
}
}
@Test
fun cancelCreateBookmarkFolderTest() {
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
clickAddFolderButton()
addNewFolderName(bookmarksFolderName, composeTestRule)
navigateUp()
verifyKeyboardHidden()
verifyBookmarkFolderIsNotCreated(bookmarksFolderName, composeTestRule)
}
}
@Test
fun threeDotMenuItemsForFolderTest() {
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
createFolder("1", composeTestRule)
}.openThreeDotMenu("1", composeTestRule) {
verifyEditButton(composeTestRule)
verifyDeleteButton(composeTestRule)
}
}
@Test
fun threeDotMenuItemsForSiteTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
}.openThreeDotMenu(defaultWebPage.url, composeTestRule) {
verifyEditButton(composeTestRule)
verifyCopyButton(composeTestRule)
verifyShareButton(composeTestRule)
verifyOpenInNewTabButton(composeTestRule)
verifyOpenInPrivateTabButton(composeTestRule)
verifyDeleteButton(composeTestRule)
}
}
@SmokeTest
@Test
fun editBookmarkTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
}.openThreeDotMenu(defaultWebPage.url, composeTestRule) {
}.clickEdit(composeTestRule) {
verifyEditBookmarksView()
verifyBookmarkNameEditBox(composeTestRule)
verifyBookmarkUrlEditBox(composeTestRule)
verifyParentFolderSelector(composeTestRule)
changeBookmarkTitle(testBookmark.title, composeTestRule)
changeBookmarkUrl(testBookmark.url, composeTestRule)
saveEditBookmark()
verifyBookmarkTitle(testBookmark.title, composeTestRule)
verifyBookmarkedUrl(testBookmark.url, composeTestRule)
verifyKeyboardHidden()
}
}
@Test
fun copyBookmarkUrlTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
}.openThreeDotMenu(defaultWebPage.url, composeTestRule) {
}.clickCopy(composeTestRule) {
verifyCopySnackBarText()
navigateUp()
}
navigationToolbar {
}.clickUrlbar {
clickClearButton()
longClickToolbar()
clickPasteText()
verifyPastedToolbarText(defaultWebPage.url.toString())
}
}
@Test
fun threeDotMenuShareBookmarkTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
}.openThreeDotMenu(defaultWebPage.url, composeTestRule) {
}.clickShare(composeTestRule) {
verifyShareOverlay()
verifyShareBookmarkFavicon()
verifyShareBookmarkTitle()
verifyShareBookmarkUrl()
}
}
@Test
fun openBookmarkInNewTabTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
}.openThreeDotMenu(defaultWebPage.url, composeTestRule) {
}.clickOpenInNewTab(composeTestRule) {
verifyTabTrayIsOpened()
verifyNormalModeSelected()
}
}
@Test
fun openBookmarkInPrivateTabTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
}.openThreeDotMenu(defaultWebPage.url, composeTestRule) {
}.clickOpenInPrivateTab(composeTestRule) {
verifyTabTrayIsOpened()
verifyPrivateModeSelected()
}
}
@SmokeTest
@Test
fun deleteBookmarkTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
}.openThreeDotMenu(defaultWebPage.url, composeTestRule) {
verifyDeleteButtonStyle(composeTestRule)
}.clickDelete(composeTestRule) {
verifyDeleteSnackBarText()
verifyUndoDeleteSnackBarButton()
}
}
@SmokeTest
@Test
fun undoDeleteBookmarkTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
}.openThreeDotMenu(defaultWebPage.url, composeTestRule) {
}.clickDelete(composeTestRule) {
verifyUndoDeleteSnackBarButton()
clickUndoDeleteButton()
verifySnackBarHidden()
verifyBookmarkedUrl(defaultWebPage.url.toString(), composeTestRule)
}
}
@SmokeTest
@Test
fun bookmarksMultiSelectionToolbarItemsTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
longTapSelectItem(defaultWebPage.url, composeTestRule)
}
multipleSelectionToolbar {
verifyMultiSelectionCheckmark(defaultWebPage.url, composeTestRule)
verifyMultiSelectionCounter()
verifyShareBookmarksButton()
verifyCloseToolbarButton()
}.closeToolbarReturnToBookmarks {
verifyBookmarksMenuView()
}
}
@SmokeTest
@Test
fun openSelectionInNewTabTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openTabDrawer {
closeTab()
}
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
longTapSelectItem(defaultWebPage.url, composeTestRule)
openActionBarOverflowOrOptionsMenu(activity)
}
multipleSelectionToolbar {
}.clickOpenNewTab {
verifyNormalModeSelected()
verifyExistingTabList()
}
}
@SmokeTest
@Test
fun openSelectionInPrivateTabTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
longTapSelectItem(defaultWebPage.url, composeTestRule)
openActionBarOverflowOrOptionsMenu(activity)
}
multipleSelectionToolbar {
}.clickOpenPrivateTab {
verifyPrivateModeSelected()
verifyExistingTabList()
}
}
@SmokeTest
@Test
fun deleteMultipleSelectionTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
browserScreen {
createBookmark(firstWebPage.url)
createBookmark(secondWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
longTapSelectItem(firstWebPage.url, composeTestRule)
tapSelectItem(secondWebPage.url, composeTestRule)
openActionBarOverflowOrOptionsMenu(activity)
}
multipleSelectionToolbar {
clickMultiSelectionDelete()
}
bookmarksMenu {
verifyDeleteMultipleBookmarksSnackBar()
}
}
@SmokeTest
@Test
fun undoDeleteMultipleSelectionTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
browserScreen {
createBookmark(firstWebPage.url)
createBookmark(secondWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
longTapSelectItem(firstWebPage.url, composeTestRule)
tapSelectItem(secondWebPage.url, composeTestRule)
openActionBarOverflowOrOptionsMenu(activity)
}
multipleSelectionToolbar {
clickMultiSelectionDelete()
}
bookmarksMenu {
verifyDeleteMultipleBookmarksSnackBar()
clickUndoDeleteButton()
verifyBookmarkedUrl(firstWebPage.url.toString(), composeTestRule)
verifyBookmarkedUrl(secondWebPage.url.toString(), composeTestRule)
}
}
@Test
fun multipleSelectionShareButtonTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
longTapSelectItem(defaultWebPage.url, composeTestRule)
}
multipleSelectionToolbar {
clickShareBookmarksButton()
verifyShareOverlay()
verifyShareTabFavicon()
verifyShareTabTitle()
verifyShareTabUrl()
}
}
@Test
fun multipleBookmarkDeletionsTest() {
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
createFolder("1", composeTestRule)
createFolder("2", composeTestRule)
createFolder("3", composeTestRule)
}.openThreeDotMenu("1", composeTestRule) {
}.clickDelete(composeTestRule) {
verifyDeleteFolderConfirmationMessage()
confirmDeletion()
verifyDeleteSnackBarText()
}.openThreeDotMenu("2", composeTestRule) {
}.clickDelete(composeTestRule) {
verifyDeleteFolderConfirmationMessage()
confirmDeletion()
verifyDeleteSnackBarText()
verifyFolderTitle("3", composeTestRule)
// On some devices we need to wait for the Snackbar to be gone before continuing
TestHelper.waitUntilSnackbarGone()
}.closeMenu {
}
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
verifyFolderTitle("3", composeTestRule)
}
}
@SmokeTest
@Test
fun changeBookmarkParentFolderTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
createFolder(bookmarksFolderName, composeTestRule)
}.openThreeDotMenu(defaultWebPage.title, composeTestRule) {
}.clickEdit(composeTestRule) {
clickParentFolderSelector(composeTestRule)
selectFolder(bookmarksFolderName, composeTestRule)
navigateUp()
saveEditBookmark()
selectFolder(bookmarksFolderName, composeTestRule)
verifyBookmarkedUrl(defaultWebPage.url.toString(), composeTestRule)
}
}
@Test
fun navigateBookmarksFoldersTest() {
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
createFolder("1", composeTestRule)
waitForBookmarksFolderContentToExist("Bookmarks", "1")
selectFolder("1", composeTestRule)
verifyCurrentFolderTitle("1")
createFolder("2", composeTestRule)
waitForBookmarksFolderContentToExist("1", "2")
selectFolder("2", composeTestRule)
verifyCurrentFolderTitle("2")
navigateUp()
waitForBookmarksFolderContentToExist("1", "2")
verifyCurrentFolderTitle("1")
mDevice.pressBack()
verifyBookmarksMenuView()
}
}
@Test
fun cantSelectDesktopFoldersTest() {
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
longTapDesktopFolder(composeTestRule)
verifySelectDefaultFolderSnackBarText()
}
}
@Test
fun verifyCloseMenuTest() {
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
}.closeMenu {
verifyHomeScreen()
}
}
@Test
fun deleteBookmarkInEditModeTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
}.openThreeDotMenu(defaultWebPage.url, composeTestRule) {
}.clickEdit(composeTestRule) {
clickDeleteInEditModeButton()
cancelDeletion()
clickDeleteInEditModeButton()
confirmDeletion()
verifyDeleteSnackBarText()
verifyBookmarkIsDeleted("Test_Page_1", composeTestRule)
}
}
@SmokeTest
@Test
fun undoDeleteBookmarkFolderTest() {
browserScreen {
}.openThreeDotMenu {
}.openBookmarks {
createFolder("My Folder", composeTestRule)
verifyFolderTitle("My Folder", composeTestRule)
}.openThreeDotMenu("My Folder", composeTestRule) {
}.clickDelete(composeTestRule) {
cancelFolderDeletion()
verifyFolderTitle("My Folder", composeTestRule)
}.openThreeDotMenu("My Folder", composeTestRule) {
}.clickDelete(composeTestRule) {
confirmDeletion()
verifyDeleteSnackBarText()
clickUndoDeleteButton()
verifyFolderTitle("My Folder", composeTestRule)
}
}
}

View File

@ -0,0 +1,93 @@
/* 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 net.waterfox.android.ui
import androidx.core.net.toUri
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import net.waterfox.android.R
import net.waterfox.android.customannotations.SmokeTest
import net.waterfox.android.helpers.FeatureSettingsHelper
import net.waterfox.android.helpers.HomeActivityTestRule
import net.waterfox.android.helpers.RetryTestRule
import net.waterfox.android.helpers.TestHelper.getStringResource
import net.waterfox.android.ui.robots.navigationToolbar
/**
* Tests that verify errors encountered while browsing websites: unsafe pages, connection errors, etc
*/
class BrowsingErrorPagesTest {
private val malwareWarning = getStringResource(R.string.mozac_browser_errorpages_safe_browsing_malware_uri_title)
private val phishingWarning = getStringResource(R.string.mozac_browser_errorpages_safe_phishing_uri_title)
private val unwantedSoftwareWarning =
getStringResource(R.string.mozac_browser_errorpages_safe_browsing_unwanted_uri_title)
private val harmfulSiteWarning = getStringResource(R.string.mozac_browser_errorpages_safe_harmful_uri_title)
private val featureSettingsHelper = FeatureSettingsHelper()
@get: Rule
val mActivityTestRule = HomeActivityTestRule()
@Rule
@JvmField
val retryTestRule = RetryTestRule(3)
@Before
fun setUp() {
// disabling the jump-back-in pop-up that interferes with the tests.
featureSettingsHelper.setJumpBackCFREnabled(false)
featureSettingsHelper.setTCPCFREnabled(false)
}
@After
fun tearDown() {
featureSettingsHelper.resetAllFeatureFlags()
}
@SmokeTest
@Test
fun blockMalwarePageTest() {
val malwareURl = "http://itisatrap.org/firefox/its-an-attack.html"
navigationToolbar {
}.enterURLAndEnterToBrowser(malwareURl.toUri()) {
verifyPageContent(malwareWarning)
}
}
@SmokeTest
@Test
fun blockPhishingPageTest() {
val phishingURl = "http://itisatrap.org/firefox/its-a-trap.html"
navigationToolbar {
}.enterURLAndEnterToBrowser(phishingURl.toUri()) {
verifyPageContent(phishingWarning)
}
}
@SmokeTest
@Test
fun blockUnwantedSoftwarePageTest() {
val unwantedURl = "http://itisatrap.org/firefox/unwanted.html"
navigationToolbar {
}.enterURLAndEnterToBrowser(unwantedURl.toUri()) {
verifyPageContent(unwantedSoftwareWarning)
}
}
@SmokeTest
@Test
fun blockHarmfulPageTest() {
val harmfulURl = "https://itisatrap.org/firefox/harmful.html"
navigationToolbar {
}.enterURLAndEnterToBrowser(harmfulURl.toUri()) {
verifyPageContent(harmfulSiteWarning)
}
}
}

View File

@ -0,0 +1,504 @@
/* 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 net.waterfox.android.ui
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
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 net.waterfox.android.customannotations.SmokeTest
import net.waterfox.android.helpers.AndroidAssetDispatcher
import net.waterfox.android.helpers.FeatureSettingsHelper
import net.waterfox.android.helpers.HomeActivityIntentTestRule
import net.waterfox.android.helpers.TestAssetHelper.getGenericAsset
import net.waterfox.android.ui.robots.browserScreen
import net.waterfox.android.ui.robots.collectionRobot
import net.waterfox.android.ui.robots.homeScreen
import net.waterfox.android.ui.robots.navigationToolbar
import net.waterfox.android.ui.robots.tabDrawer
/**
* Tests for verifying basic functionality of tab collections
*
*/
class CollectionTest {
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer
private val firstCollectionName = "testcollection_1"
private val secondCollectionName = "testcollection_2"
private val collectionName = "First Collection"
private val featureSettingsHelper = FeatureSettingsHelper()
@get:Rule
val composeTestRule = AndroidComposeTestRule(
HomeActivityIntentTestRule(),
{ it.activity }
)
@Before
fun setUp() {
// disabling these features to have better visibility of Collections,
// and to avoid multiple matches on tab items
featureSettingsHelper.setRecentTabsFeatureEnabled(false)
featureSettingsHelper.setJumpBackCFREnabled(false)
featureSettingsHelper.setRecentlyVisitedFeatureEnabled(false)
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@After
fun tearDown() {
mockWebServer.shutdown()
// resetting modified features enabled setting to default
featureSettingsHelper.resetAllFeatureFlags()
}
@SmokeTest
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun createFirstCollectionTest() {
val firstWebPage = getGenericAsset(mockWebServer, 1)
val secondWebPage = getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
mDevice.waitForIdle()
}.openTabDrawer {
}.openNewTab {
}.submitQuery(secondWebPage.url.toString()) {
mDevice.waitForIdle()
}.goToHomescreen {
swipeToBottom()
}.clickSaveTabsToCollectionButton {
longClickTab(firstWebPage.title)
selectTab(secondWebPage.title, numOfTabs = 2)
}.clickSaveCollection {
typeCollectionNameAndSave(collectionName)
}
tabDrawer {
verifySnackBarText("Collection saved!")
snackBarButtonClick("VIEW")
}
homeScreen {
verifyCollectionIsDisplayed(collectionName)
}
}
@SmokeTest
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun verifyExpandedCollectionItemsTest() {
val webPage = getGenericAsset(mockWebServer, 1)
val webPageUrl = webPage.url.host.toString()
navigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openTabDrawer {
createCollection(webPage.title, collectionName = collectionName)
snackBarButtonClick("VIEW")
}
homeScreen {
verifyCollectionIsDisplayed(collectionName)
}.expandCollection(collectionName, composeTestRule) {
verifyTabSavedInCollection(webPage.title)
verifyCollectionTabUrl(true, webPageUrl)
verifyShareCollectionButtonIsVisible(true)
verifyCollectionMenuIsVisible(true, composeTestRule)
verifyCollectionItemRemoveButtonIsVisible(webPage.title, true)
}.collapseCollection(collectionName) {}
collectionRobot {
verifyTabSavedInCollection(webPage.title, false)
verifyShareCollectionButtonIsVisible(false)
verifyCollectionMenuIsVisible(false, composeTestRule)
verifyCollectionTabUrl(false, webPageUrl)
verifyCollectionItemRemoveButtonIsVisible(webPage.title, false)
}
homeScreen {
}.expandCollection(collectionName, composeTestRule) {
verifyTabSavedInCollection(webPage.title)
verifyCollectionTabUrl(true, webPageUrl)
verifyShareCollectionButtonIsVisible(true)
verifyCollectionMenuIsVisible(true, composeTestRule)
verifyCollectionItemRemoveButtonIsVisible(webPage.title, true)
}.collapseCollection(collectionName) {}
collectionRobot {
verifyTabSavedInCollection(webPage.title, false)
verifyShareCollectionButtonIsVisible(false)
verifyCollectionMenuIsVisible(false, composeTestRule)
verifyCollectionTabUrl(false, webPageUrl)
verifyCollectionItemRemoveButtonIsVisible(webPage.title, false)
}
}
@SmokeTest
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun openAllTabsInCollectionTest() {
val firstTestPage = getGenericAsset(mockWebServer, 1)
val secondTestPage = getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstTestPage.url) {
waitForPageToLoad()
}.openTabDrawer {
}.openNewTab {
}.submitQuery(secondTestPage.url.toString()) {
waitForPageToLoad()
}.openTabDrawer {
createCollection(
firstTestPage.title,
secondTestPage.title,
collectionName = collectionName
)
closeTab()
}
homeScreen {
}.expandCollection(collectionName, composeTestRule) {
clickCollectionThreeDotButton(composeTestRule)
selectOpenTabs(composeTestRule)
}
tabDrawer {
verifyExistingOpenTabs(firstTestPage.title, secondTestPage.title)
}
}
@SmokeTest
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun shareCollectionTest() {
val firstWebsite = getGenericAsset(mockWebServer, 1)
val secondWebsite = getGenericAsset(mockWebServer, 2)
val sharingApp = "Gmail"
val urlString = "${secondWebsite.url}\n\n${firstWebsite.url}"
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebsite.url) {
}.openTabDrawer {
}.openNewTab {
}.submitQuery(secondWebsite.url.toString()) {
waitForPageToLoad()
}.openTabDrawer {
createCollection(firstWebsite.title, secondWebsite.title, collectionName = collectionName)
verifySnackBarText("Collection saved!")
}.openTabsListThreeDotMenu {
}.closeAllTabs {
}.expandCollection(collectionName, composeTestRule) {
}.clickShareCollectionButton {
verifyShareTabsOverlay(firstWebsite.title, secondWebsite.title)
verifySharingWithSelectedApp(sharingApp, urlString, collectionName)
}
}
@SmokeTest
@Test
// Test running on release builds in CI:
// caution when making changes to it, so they don't block the builds
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun deleteCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openTabDrawer {
createCollection(webPage.title, collectionName = collectionName)
snackBarButtonClick("VIEW")
}
homeScreen {
}.expandCollection(collectionName, composeTestRule) {
clickCollectionThreeDotButton(composeTestRule)
selectDeleteCollection(composeTestRule)
}
homeScreen {
verifySnackBarText("Collection deleted")
verifyNoCollectionsText()
}
}
@Test
// open a webpage, and add currently opened tab to existing collection
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun mainMenuSaveToExistingCollection() {
val firstWebPage = getGenericAsset(mockWebServer, 1)
val secondWebPage = getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
}.openTabDrawer {
createCollection(firstWebPage.title, collectionName = collectionName)
verifySnackBarText("Collection saved!")
}.closeTabDrawer {}
navigationToolbar {
}.enterURLAndEnterToBrowser(secondWebPage.url) {
verifyPageContent(secondWebPage.content)
}.openThreeDotMenu {
}.openSaveToCollection {
}.selectExistingCollection(collectionName) {
verifySnackBarText("Tab saved!")
}.goToHomescreen {
}.expandCollection(collectionName, composeTestRule) {
verifyTabSavedInCollection(firstWebPage.title)
verifyTabSavedInCollection(secondWebPage.title)
}
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun verifyAddTabButtonOfCollectionMenu() {
val firstWebPage = getGenericAsset(mockWebServer, 1)
val secondWebPage = getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
}.openTabDrawer {
createCollection(firstWebPage.title, collectionName = collectionName)
verifySnackBarText("Collection saved!")
closeTab()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(secondWebPage.url) {
}.goToHomescreen {
}.expandCollection(collectionName, composeTestRule) {
clickCollectionThreeDotButton(composeTestRule)
selectAddTabToCollection(composeTestRule)
verifyTabsSelectedCounterText(1)
saveTabsSelectedForCollection()
verifySnackBarText("Tab saved!")
verifyTabSavedInCollection(secondWebPage.title)
}
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun renameCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openTabDrawer {
createCollection(webPage.title, collectionName = firstCollectionName)
verifySnackBarText("Collection saved!")
}.closeTabDrawer {
}.goToHomescreen {
}.expandCollection(firstCollectionName, composeTestRule) {
clickCollectionThreeDotButton(composeTestRule)
selectRenameCollection(composeTestRule)
}.typeCollectionNameAndSave(secondCollectionName) {}
homeScreen {
verifyCollectionIsDisplayed(secondCollectionName)
}
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun createSecondCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openTabDrawer {
createCollection(webPage.title, collectionName = firstCollectionName)
verifySnackBarText("Collection saved!")
createCollection(
webPage.title, collectionName = secondCollectionName,
firstCollection = false
)
verifySnackBarText("Collection saved!")
}.closeTabDrawer {
}.goToHomescreen {
verifyCollectionIsDisplayed(firstCollectionName)
verifyCollectionIsDisplayed(secondCollectionName)
}
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun removeTabFromCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openTabDrawer {
createCollection(webPage.title, collectionName = collectionName)
closeTab()
}
homeScreen {
}.expandCollection(collectionName, composeTestRule) {
verifyTabSavedInCollection(webPage.title, true)
removeTabFromCollection(webPage.title)
verifyTabSavedInCollection(webPage.title, false)
}
homeScreen {
verifyCollectionIsDisplayed(collectionName, false)
}
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun swipeLeftToRemoveTabFromCollectionTest() {
val testPage = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(testPage.url) {
waitForPageToLoad()
}.openTabDrawer {
createCollection(
testPage.title,
collectionName = collectionName
)
closeTab()
}
homeScreen {
}.expandCollection(collectionName, composeTestRule) {
swipeToBottom()
swipeTabLeft(testPage.title, composeTestRule)
verifyTabSavedInCollection(testPage.title, false)
}
homeScreen {
verifyCollectionIsDisplayed(collectionName, false)
}
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun swipeRightToRemoveTabFromCollectionTest() {
val testPage = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(testPage.url) {
waitForPageToLoad()
}.openTabDrawer {
createCollection(
testPage.title,
collectionName = collectionName
)
closeTab()
}
homeScreen {
}.expandCollection(collectionName, composeTestRule) {
swipeToBottom()
swipeTabRight(testPage.title, composeTestRule)
verifyTabSavedInCollection(testPage.title, false)
}
homeScreen {
verifyCollectionIsDisplayed(collectionName, false)
}
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun selectTabOnLongTapTest() {
val firstWebPage = getGenericAsset(mockWebServer, 1)
val secondWebPage = getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
waitForPageToLoad()
}.openTabDrawer {
}.openNewTab {
}.submitQuery(secondWebPage.url.toString()) {
waitForPageToLoad()
}.openTabDrawer {
verifyExistingOpenTabs(firstWebPage.title, secondWebPage.title)
longClickTab(firstWebPage.title)
verifyTabsMultiSelectionCounter(1)
selectTab(secondWebPage.title, numOfTabs = 2)
}.clickSaveCollection {
typeCollectionNameAndSave(collectionName)
verifySnackBarText("Tabs saved!")
}
tabDrawer {
}.closeTabDrawer {
}.goToHomescreen {
}.expandCollection(collectionName, composeTestRule) {
verifyTabSavedInCollection(firstWebPage.title)
verifyTabSavedInCollection(secondWebPage.title)
}
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun navigateBackInCollectionFlowTest() {
val webPage = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openTabDrawer {
createCollection(webPage.title, collectionName = collectionName)
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()
}
}
@SmokeTest
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun undoDeleteCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openTabDrawer {
createCollection(webPage.title, collectionName = collectionName)
snackBarButtonClick("VIEW")
}
homeScreen {
}.expandCollection(collectionName, composeTestRule) {
clickCollectionThreeDotButton(composeTestRule)
selectDeleteCollection(composeTestRule)
}
homeScreen {
verifySnackBarText("Collection deleted")
clickUndoCollectionDeletion("UNDO")
verifyCollectionIsDisplayed(collectionName, true)
}
}
}

View File

@ -0,0 +1,285 @@
/* 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 net.waterfox.android.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 net.waterfox.android.customannotations.SmokeTest
import net.waterfox.android.ext.settings
import net.waterfox.android.helpers.AndroidAssetDispatcher
import net.waterfox.android.helpers.FeatureSettingsHelper
import net.waterfox.android.helpers.HomeActivityIntentTestRule
import net.waterfox.android.helpers.RetryTestRule
import net.waterfox.android.helpers.TestAssetHelper
import net.waterfox.android.ui.robots.downloadRobot
import net.waterfox.android.ui.robots.homeScreen
import net.waterfox.android.ui.robots.navigationToolbar
/**
* Tests for verifying basic functionality of content context menus
*
* - Verifies long click "Open link in new tab" UI and functionality
* - Verifies long click "Open link in new Private tab" UI and functionality
* - Verifies long click "Copy Link" UI and functionality
* - Verifies long click "Share Link" UI and functionality
* - Verifies long click "Open image in new tab" UI and functionality
* - Verifies long click "Save Image" UI and functionality
* - Verifies long click "Copy image location" UI and functionality
* - Verifies long click items of mixed hypertext items
*
*/
class ContextMenusTest {
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer
@get:Rule
val activityIntentTestRule = HomeActivityIntentTestRule()
@Rule
@JvmField
val retryTestRule = RetryTestRule(3)
private val featureSettingsHelper = FeatureSettingsHelper()
@Before
fun setUp() {
activityIntentTestRule.activity.applicationContext.settings().shouldShowJumpBackInCFR = false
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
featureSettingsHelper.setTCPCFREnabled(false)
}
@After
fun tearDown() {
mockWebServer.shutdown()
featureSettingsHelper.resetAllFeatureFlags()
}
@SmokeTest
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun verifyContextOpenLinkNewTab() {
val pageLinks =
TestAssetHelper.getGenericAsset(mockWebServer, 4)
val genericURL =
TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(pageLinks.url) {
mDevice.waitForIdle()
longClickMatchingText("Link 1")
verifyLinkContextMenuItems(genericURL.url)
clickContextOpenLinkInNewTab()
verifySnackBarText("New tab opened")
snackBarButtonClick()
verifyUrl(genericURL.url.toString())
}.openTabDrawer {
verifyNormalModeSelected()
verifyExistingOpenTabs("Test_Page_1")
verifyExistingOpenTabs("Test_Page_4")
}
}
@SmokeTest
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun verifyContextOpenLinkPrivateTab() {
val pageLinks =
TestAssetHelper.getGenericAsset(mockWebServer, 4)
val genericURL =
TestAssetHelper.getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(pageLinks.url) {
mDevice.waitForIdle()
longClickMatchingText("Link 2")
verifyLinkContextMenuItems(genericURL.url)
clickContextOpenLinkInPrivateTab()
verifySnackBarText("New private tab opened")
snackBarButtonClick()
verifyUrl(genericURL.url.toString())
}.openTabDrawer {
verifyPrivateModeSelected()
verifyExistingOpenTabs("Test_Page_2")
}
}
@Test
fun verifyContextCopyLink() {
val pageLinks =
TestAssetHelper.getGenericAsset(mockWebServer, 4)
val genericURL =
TestAssetHelper.getGenericAsset(mockWebServer, 3)
navigationToolbar {
}.enterURLAndEnterToBrowser(pageLinks.url) {
mDevice.waitForIdle()
longClickMatchingText("Link 3")
verifyLinkContextMenuItems(genericURL.url)
clickContextCopyLink()
verifySnackBarText("Link copied to clipboard")
}.openNavigationToolbar {
}.visitLinkFromClipboard {
verifyUrl(genericURL.url.toString())
}
}
@Test
fun verifyContextShareLink() {
val pageLinks =
TestAssetHelper.getGenericAsset(mockWebServer, 4)
val genericURL =
TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(pageLinks.url) {
mDevice.waitForIdle()
longClickMatchingText("Link 1")
verifyLinkContextMenuItems(genericURL.url)
clickContextShareLink(genericURL.url) // verify share intent is matched with associated URL
}
}
@Test
fun verifyContextOpenImageNewTab() {
val pageLinks =
TestAssetHelper.getGenericAsset(mockWebServer, 4)
val imageResource =
TestAssetHelper.getImageAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(pageLinks.url) {
mDevice.waitForIdle()
longClickMatchingText("test_link_image")
verifyLinkImageContextMenuItems(imageResource.url)
clickContextOpenImageNewTab()
verifySnackBarText("New tab opened")
snackBarButtonClick()
verifyUrl(imageResource.url.toString())
}
}
@Test
fun verifyContextCopyImageLocation() {
val pageLinks =
TestAssetHelper.getGenericAsset(mockWebServer, 4)
val imageResource =
TestAssetHelper.getImageAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(pageLinks.url) {
mDevice.waitForIdle()
longClickMatchingText("test_link_image")
verifyLinkImageContextMenuItems(imageResource.url)
clickContextCopyImageLocation()
verifySnackBarText("Link copied to clipboard")
}.openNavigationToolbar {
}.visitLinkFromClipboard {
verifyUrl(imageResource.url.toString())
}
}
@Test
fun verifyContextSaveImage() {
val pageLinks =
TestAssetHelper.getGenericAsset(mockWebServer, 4)
val imageResource =
TestAssetHelper.getImageAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(pageLinks.url) {
mDevice.waitForIdle()
longClickMatchingText("test_link_image")
verifyLinkImageContextMenuItems(imageResource.url)
clickContextSaveImage()
}
downloadRobot {
verifyDownloadNotificationPopup()
}.clickOpen("image/jpeg") {} // verify open intent is matched with associated data type
downloadRobot {
verifyPhotosAppOpens()
}
}
@Ignore("Failing with frequent ANR: https://bugzilla.mozilla.org/show_bug.cgi?id=1764605")
@Test
fun verifyContextMixedVariations() {
val pageLinks =
TestAssetHelper.getGenericAsset(mockWebServer, 4)
val genericURL =
TestAssetHelper.getGenericAsset(mockWebServer, 1)
val imageResource =
TestAssetHelper.getImageAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(pageLinks.url) {
mDevice.waitForIdle()
longClickMatchingText("Link 1")
verifyLinkContextMenuItems(genericURL.url)
dismissContentContextMenu(genericURL.url)
longClickMatchingText("test_link_image")
verifyLinkImageContextMenuItems(imageResource.url)
dismissContentContextMenu(imageResource.url)
longClickMatchingText("test_no_link_image")
verifyNoLinkImageContextMenuItems(imageResource.url)
}
}
@SmokeTest
@Test
fun shareSelectedTextTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
longClickMatchingText(genericURL.content)
}.clickShareSelectedText {
verifyAndroidShareLayout()
}
}
@SmokeTest
@Test
fun selectAndSearchTextTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
longClickAndSearchText("Search", "content")
mDevice.waitForIdle()
verifyTabCounter("2")
verifyUrl("google")
}
}
@SmokeTest
@Test
fun privateSelectAndSearchTextTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.togglePrivateBrowsingMode()
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
longClickAndSearchText("Private Search", "content")
mDevice.waitForIdle()
verifyTabCounter("2")
verifyUrl("google")
}
}
}

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