v133
commit
37a045067d
|
@ -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}]
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
CHANGELOG.md merge=union
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
||||
-->
|
|
@ -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/)")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
|
@ -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/
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
|||
/build
|
|
@ -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'
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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>
|
|
@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,10 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Test_Page_1</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>
|
||||
<p id="testContent">Page content: 1</p>
|
||||
</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,10 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Test_Page_2</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>
|
||||
<p id="testContent">Page content: 2</p>
|
||||
</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -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>
|
|
@ -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
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 |
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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)"
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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, "", "")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
|
@ -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))
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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++
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
||||
"""
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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"
|
673
app/src/androidTest/java/net/waterfox/android/syncintegration/Pipfile.lock
generated
Normal file
673
app/src/androidTest/java/net/waterfox/android/syncintegration/Pipfile.lock
generated
Normal 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": {}
|
||||
}
|
|
@ -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())
|
|
@ -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")
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
[pytest]
|
||||
addopts = --verbose --html=results/index.html
|
||||
log_cli = true
|
||||
log_cli_level = info
|
|
@ -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]
|
||||
]);
|
|
@ -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],
|
||||
]);
|
|
@ -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]
|
||||
]);
|
|
@ -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]
|
||||
]);
|
|
@ -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')
|
||||
'''
|
|
@ -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]
|
||||
]);
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue