first commit
commit
52b902effa
|
@ -0,0 +1,154 @@
|
||||||
|
projects:
|
||||||
|
app:
|
||||||
|
upstream_dependencies:
|
||||||
|
- browser-domains
|
||||||
|
- browser-engine-gecko
|
||||||
|
- browser-errorpages
|
||||||
|
- browser-icons
|
||||||
|
- browser-menu
|
||||||
|
- browser-menu2
|
||||||
|
- browser-session-storage
|
||||||
|
- browser-state
|
||||||
|
- browser-storage-sync
|
||||||
|
- browser-tabstray
|
||||||
|
- browser-thumbnails
|
||||||
|
- browser-toolbar
|
||||||
|
- compose-awesomebar
|
||||||
|
- compose-cfr
|
||||||
|
- concept-awesomebar
|
||||||
|
- concept-base
|
||||||
|
- concept-engine
|
||||||
|
- concept-fetch
|
||||||
|
- concept-menu
|
||||||
|
- concept-push
|
||||||
|
- concept-storage
|
||||||
|
- concept-sync
|
||||||
|
- concept-tabstray
|
||||||
|
- concept-toolbar
|
||||||
|
- feature-accounts
|
||||||
|
- feature-accounts-push
|
||||||
|
- feature-addons
|
||||||
|
- feature-app-links
|
||||||
|
- feature-autofill
|
||||||
|
- feature-awesomebar
|
||||||
|
- feature-contextmenu
|
||||||
|
- feature-customtabs
|
||||||
|
- feature-downloads
|
||||||
|
- feature-findinpage
|
||||||
|
- feature-fxsuggest
|
||||||
|
- feature-intent
|
||||||
|
- feature-logins
|
||||||
|
- feature-media
|
||||||
|
- feature-privatemode
|
||||||
|
- feature-prompts
|
||||||
|
- feature-push
|
||||||
|
- feature-pwa
|
||||||
|
- feature-qr
|
||||||
|
- feature-readerview
|
||||||
|
- feature-recentlyclosed
|
||||||
|
- feature-search
|
||||||
|
- feature-session
|
||||||
|
- feature-share
|
||||||
|
- feature-sitepermissions
|
||||||
|
- feature-syncedtabs
|
||||||
|
- feature-tab-collections
|
||||||
|
- feature-tabs
|
||||||
|
- feature-toolbar
|
||||||
|
- feature-top-sites
|
||||||
|
- feature-webauthn
|
||||||
|
- feature-webcompat
|
||||||
|
- feature-webcompat-reporter
|
||||||
|
- feature-webnotifications
|
||||||
|
- lib-crash
|
||||||
|
- lib-crash-sentry
|
||||||
|
- lib-dataprotect
|
||||||
|
- lib-publicsuffixlist
|
||||||
|
- lib-push-firebase
|
||||||
|
- lib-state
|
||||||
|
- service-contile
|
||||||
|
- service-digitalassetlinks
|
||||||
|
- service-firefox-accounts
|
||||||
|
- service-glean
|
||||||
|
- service-location
|
||||||
|
- service-nimbus
|
||||||
|
- service-pocket
|
||||||
|
- service-sync-autofill
|
||||||
|
- service-sync-logins
|
||||||
|
- support-base
|
||||||
|
- support-images
|
||||||
|
- support-ktx
|
||||||
|
- support-locale
|
||||||
|
- support-remotesettings
|
||||||
|
- support-rusterrors
|
||||||
|
- support-rusthttp
|
||||||
|
- support-rustlog
|
||||||
|
- support-test
|
||||||
|
- support-test-libstate
|
||||||
|
- support-utils
|
||||||
|
- support-webextensions
|
||||||
|
- ui-autocomplete
|
||||||
|
- ui-colors
|
||||||
|
- ui-icons
|
||||||
|
- ui-tabcounter
|
||||||
|
- ui-widgets
|
||||||
|
variants:
|
||||||
|
- apks:
|
||||||
|
- abi: arm64-v8a
|
||||||
|
fileName: app-fenix-arm64-v8a-debug.apk
|
||||||
|
- abi: armeabi-v7a
|
||||||
|
fileName: app-fenix-armeabi-v7a-debug.apk
|
||||||
|
- abi: x86
|
||||||
|
fileName: app-fenix-x86-debug.apk
|
||||||
|
- abi: x86_64
|
||||||
|
fileName: app-fenix-x86_64-debug.apk
|
||||||
|
build_type: debug
|
||||||
|
name: fenixDebug
|
||||||
|
- apks:
|
||||||
|
- abi: arm64-v8a
|
||||||
|
fileName: app-fenix-arm64-v8a-release-unsigned.apk
|
||||||
|
- abi: armeabi-v7a
|
||||||
|
fileName: app-fenix-armeabi-v7a-release-unsigned.apk
|
||||||
|
- abi: x86
|
||||||
|
fileName: app-fenix-x86-release-unsigned.apk
|
||||||
|
- abi: x86_64
|
||||||
|
fileName: app-fenix-x86_64-release-unsigned.apk
|
||||||
|
build_type: release
|
||||||
|
name: fenixRelease
|
||||||
|
- apks:
|
||||||
|
- abi: arm64-v8a
|
||||||
|
fileName: app-fenix-arm64-v8a-nightly-unsigned.apk
|
||||||
|
- abi: armeabi-v7a
|
||||||
|
fileName: app-fenix-armeabi-v7a-nightly-unsigned.apk
|
||||||
|
- abi: x86
|
||||||
|
fileName: app-fenix-x86-nightly-unsigned.apk
|
||||||
|
- abi: x86_64
|
||||||
|
fileName: app-fenix-x86_64-nightly-unsigned.apk
|
||||||
|
build_type: nightly
|
||||||
|
name: fenixNightly
|
||||||
|
- apks:
|
||||||
|
- abi: arm64-v8a
|
||||||
|
fileName: app-fenix-arm64-v8a-beta-unsigned.apk
|
||||||
|
- abi: armeabi-v7a
|
||||||
|
fileName: app-fenix-armeabi-v7a-beta-unsigned.apk
|
||||||
|
- abi: x86
|
||||||
|
fileName: app-fenix-x86-beta-unsigned.apk
|
||||||
|
- abi: x86_64
|
||||||
|
fileName: app-fenix-x86_64-beta-unsigned.apk
|
||||||
|
build_type: beta
|
||||||
|
name: fenixBeta
|
||||||
|
- apks:
|
||||||
|
- abi: arm64-v8a
|
||||||
|
fileName: app-fenix-arm64-v8a-benchmark-unsigned.apk
|
||||||
|
- abi: armeabi-v7a
|
||||||
|
fileName: app-fenix-armeabi-v7a-benchmark-unsigned.apk
|
||||||
|
- abi: x86
|
||||||
|
fileName: app-fenix-x86-benchmark-unsigned.apk
|
||||||
|
- abi: x86_64
|
||||||
|
fileName: app-fenix-x86_64-benchmark-unsigned.apk
|
||||||
|
build_type: benchmark
|
||||||
|
name: fenixBenchmark
|
||||||
|
- apks:
|
||||||
|
- abi: noarch
|
||||||
|
fileName: app-debug-androidTest.apk
|
||||||
|
build_type: androidTest
|
||||||
|
name: androidTest
|
|
@ -0,0 +1,6 @@
|
||||||
|
[*.{kt,kts}]
|
||||||
|
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,107 @@
|
||||||
|
|
||||||
|
# Created by https://www.gitignore.io/api/android
|
||||||
|
# Edit at https://www.gitignore.io/?templates=android
|
||||||
|
|
||||||
|
### Android ###
|
||||||
|
# Built application files
|
||||||
|
*.apk
|
||||||
|
*.ap_
|
||||||
|
*.aab
|
||||||
|
output-metadata.json
|
||||||
|
|
||||||
|
# Files for the ART/Dalvik VM
|
||||||
|
*.dex
|
||||||
|
|
||||||
|
# Java class files
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
bin/
|
||||||
|
gen/
|
||||||
|
out/
|
||||||
|
|
||||||
|
# Gradle files
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
!taskcluster/ci/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
|
||||||
|
.adjust_token
|
||||||
|
.sentry_token
|
||||||
|
.mls_token
|
||||||
|
.nimbus
|
||||||
|
.wallpaper_url
|
||||||
|
.pocket_consumer_key
|
||||||
|
|
||||||
|
# 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,3 @@
|
||||||
|
[submodule "android-components"]
|
||||||
|
path = android-components
|
||||||
|
url = https://github.com/akliuxingyuan/android-components
|
|
@ -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/org/mozilla/fenix/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/org/mozilla/fenix/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,31 @@
|
||||||
|
# LeOSium Browser!
|
||||||
|
|
||||||
|
Definitely not brought to you by Mozilla!
|
||||||
|
|
||||||
|
LeOSium Browser is a fork of LeOSium.... thx fork-maintainer for the sources !!!!
|
||||||
|
|
||||||
|
LeOSium Browser is a web browser for Android, based on [Mozilla's Fenix version of Firefox](https://github.com/mozilla-mobile/fenix/), [GeckoView](https://mozilla.github.io/geckoview/) and [Mozilla Android Components](https://mozac.org/).
|
||||||
|
|
||||||
|
Our goal is to be a close fork of the new Firefox for Android that seeks to provide users with more options, more opportunities to customize (including a broad extension library), and more information about the pages they visit and how their browsers are interacting with those pages.
|
||||||
|
|
||||||
|
Notable features include:
|
||||||
|
* `about:config` support
|
||||||
|
* The ability to *attempt* to install a much longer list of add-ons than Mozilla's Fenix version of Firefox accepts. Currently the browser queries [this AMO collection](https://addons.mozilla.org/en-US/firefox/collections/16201230/What-I-want-on-Fenix/) **Most of them will not work**, because they depend on code that Mozilla is still working on writing in `android-components`, but you may attempt to install them. If you don't see an add-on you want, you can [request it](https://github.com/fork-maintainers/LeOSium-browser/issues/new).
|
||||||
|
* Option to suspend tabs to avoid being killed for memory (https://bugzilla.mozilla.org/show_bug.cgi?id=1807364)
|
||||||
|
* **No warranties or guarantees of security or updates or even stability**! Note that LeOSium Browser includes some unstable code written by Mozilla, with our own added modifications on top, all shipped with the stable version of GeckoView engine. Hence, the browser may contain bugs introduced upstream. Binaries are currently built automatically by our Github release automation. These binaries are signed with a debug key. When we finally publish this somewhere official like F-droid, we will sign the apks with a proper key suitable for public release. Due to the current way we create the releases and sign them, you may not want to rely on such "alpha" quality software as your primary web browser, as it will have bugs. So, use this browser only if you are comfortable with these limitations/potential risks.
|
||||||
|
|
||||||
|
**Note/Disclaimer:** LeOSium Browser could not exist without the hardworking folks at the Mozilla Corporation who work on the Mozilla Android Components and Firefox projects, but it is not an official Mozilla product, and is not provided, endorsed, vetted, approved, or secured by Mozilla.
|
||||||
|
|
||||||
|
In addition, we intend to try to cut down on telemetry and proprietary code to as great of an extent as possible as long as doing so does not compromise the user experience or make the fork too hard to maintain. Right now, we believe that no telemetry should be being sent to Mozilla anymore, but we cannot guarantee this; data may still be sent. Because of the way we have implemented this, the app may still appear to contain trackers when analyzed by tools that look for the presence of known tracking libraries. These detected trackers should actually be non-functional substitutes, many of which are sourced [from here](https://gitlab.com/relan/fennecbuild/-/blob/master/fenix-liberate.patch). **If you catch the app actually sending data to Mozilla, Adjust, Leanplum, Firebase, or any other such service, please open an issue!** Presumably any data that reaches Mozilla is governed by Mozilla's privacy policy, but as LeOSium Browser is, again **not a Mozilla product**, we can make no promises.
|
||||||
|
|
||||||
|
LeOSium Browser combines the power of Fenix (of which we are a fork) and the spirit of Fennec, with a respectful nod toward the grand tradition of Netscape Navigator, from which all Gecko-based projects came, including the earliest of our predecessors, the old Mozilla Phoenix and Mozilla Firefox desktop browsers.
|
||||||
|
|
||||||
|
That said, LeOSium Browser is an independent all-volunteer project, and has no affiliation with Netscape, Netscape Navigator, Mozilla, Mozilla Firefox, Mozila Phoenix, Debian, Debian Iceweasel, Parabola GNU/Linux-libre Iceweasel, America Online, or Verizon, among others. :) Basically, if you don't like the browser, it's not their fault. :)
|
||||||
|
|
||||||
|
|
||||||
|
## 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 @@
|
||||||
|
Subproject commit 69f2dd41c44d9a8b690527493d1d423d857d5e97
|
|
@ -0,0 +1,239 @@
|
||||||
|
---
|
||||||
|
cookie-banners:
|
||||||
|
description: Features for cookie banner handling.
|
||||||
|
hasExposure: true
|
||||||
|
exposureDescription: ""
|
||||||
|
variables:
|
||||||
|
sections-enabled:
|
||||||
|
type: json
|
||||||
|
description: This property provides a lookup table of whether or not the given section should be enabled.
|
||||||
|
extensions-process:
|
||||||
|
description: A feature to rollout the extensions process.
|
||||||
|
hasExposure: true
|
||||||
|
exposureDescription: ""
|
||||||
|
variables:
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
description: "If true, the extensions process is enabled."
|
||||||
|
fx-suggest:
|
||||||
|
description: A feature that provides Firefox Suggest search suggestions.
|
||||||
|
hasExposure: true
|
||||||
|
exposureDescription: ""
|
||||||
|
variables:
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
description: "Whether the feature is enabled. When Firefox Suggest is enabled, Firefox will download and store new search suggestions in the background, and show additional Search settings to control which suggestions appear in the awesomebar. When Firefox Suggest is disabled, Firefox will not download new suggestions, and hide the additional Search settings.\n"
|
||||||
|
glean:
|
||||||
|
description: A feature that provides server-side configurations for Glean metrics (aka Server Knobs).
|
||||||
|
hasExposure: true
|
||||||
|
exposureDescription: ""
|
||||||
|
variables:
|
||||||
|
enable-event-timestamps:
|
||||||
|
type: boolean
|
||||||
|
description: Enables precise event timestamps for Glean events
|
||||||
|
metrics-enabled:
|
||||||
|
type: json
|
||||||
|
description: "A map of metric base-identifiers to booleans representing the state of the 'enabled' flag for that metric."
|
||||||
|
growth-data:
|
||||||
|
description: A feature measuring campaign growth data
|
||||||
|
hasExposure: true
|
||||||
|
exposureDescription: ""
|
||||||
|
variables:
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
description: "If true, the feature is active"
|
||||||
|
homescreen:
|
||||||
|
description: The homescreen that the user goes to when they press home or new tab.
|
||||||
|
hasExposure: true
|
||||||
|
exposureDescription: ""
|
||||||
|
variables:
|
||||||
|
sections-enabled:
|
||||||
|
type: json
|
||||||
|
description: "This property provides a lookup table of whether or not the given section should be enabled. If the section is enabled, it should be toggleable in the settings screen, and on by default."
|
||||||
|
juno-onboarding:
|
||||||
|
description: A feature that shows juno onboarding flow.
|
||||||
|
hasExposure: true
|
||||||
|
exposureDescription: ""
|
||||||
|
variables:
|
||||||
|
cards:
|
||||||
|
type: json
|
||||||
|
description: Collection of user facing onboarding cards.
|
||||||
|
messaging:
|
||||||
|
description: "The in-app messaging system.\n"
|
||||||
|
hasExposure: true
|
||||||
|
exposureDescription: ""
|
||||||
|
variables:
|
||||||
|
actions:
|
||||||
|
type: json
|
||||||
|
description: A growable map of action URLs.
|
||||||
|
message-under-experiment:
|
||||||
|
type: string
|
||||||
|
description: "Deprecated in favor of `MessageData#experiment`. This will be removed in future releases."
|
||||||
|
messages:
|
||||||
|
type: json
|
||||||
|
description: A growable collection of messages
|
||||||
|
notification-config:
|
||||||
|
type: json
|
||||||
|
description: Configuration of the notification worker for all notification messages.
|
||||||
|
on-control:
|
||||||
|
type: string
|
||||||
|
description: What should be displayed when a control message is selected.
|
||||||
|
enum:
|
||||||
|
- show-next-message
|
||||||
|
- show-none
|
||||||
|
styles:
|
||||||
|
type: json
|
||||||
|
description: "A map of styles to configure message appearance.\n"
|
||||||
|
triggers:
|
||||||
|
type: json
|
||||||
|
description: "A collection of out the box trigger expressions. Each entry maps to a valid JEXL expression.\n"
|
||||||
|
mr2022:
|
||||||
|
description: Features for MR 2022.
|
||||||
|
hasExposure: true
|
||||||
|
exposureDescription: ""
|
||||||
|
variables:
|
||||||
|
sections-enabled:
|
||||||
|
type: json
|
||||||
|
description: This property provides a lookup table of whether or not the given section should be enabled.
|
||||||
|
nimbus-system:
|
||||||
|
description: "Configuration of the Nimbus System in Android.\n"
|
||||||
|
hasExposure: true
|
||||||
|
exposureDescription: ""
|
||||||
|
variables:
|
||||||
|
refresh-interval-foreground:
|
||||||
|
type: int
|
||||||
|
description: "The minimum interval in minutes between fetching experiment \nrecipes in the foreground.\n"
|
||||||
|
nimbus-validation:
|
||||||
|
description: A feature that does not correspond to an application feature suitable for showing that Nimbus is working. This should never be used in production.
|
||||||
|
hasExposure: true
|
||||||
|
exposureDescription: ""
|
||||||
|
variables:
|
||||||
|
settings-icon:
|
||||||
|
type: string
|
||||||
|
description: The drawable displayed in the app menu for Settings
|
||||||
|
settings-punctuation:
|
||||||
|
type: string
|
||||||
|
description: The emoji displayed in the Settings screen title.
|
||||||
|
settings-title:
|
||||||
|
type: string
|
||||||
|
description: The title of displayed in the Settings screen and app menu.
|
||||||
|
onboarding:
|
||||||
|
description: "A feature that configures the new user onboarding page. Note that onboarding is a **first run** feature, and should only be modified by first run experiments."
|
||||||
|
hasExposure: true
|
||||||
|
exposureDescription: ""
|
||||||
|
variables:
|
||||||
|
order:
|
||||||
|
type: json
|
||||||
|
description: Determines the order of the onboarding page panels
|
||||||
|
pdfjs:
|
||||||
|
description: PDF.js features
|
||||||
|
hasExposure: true
|
||||||
|
exposureDescription: ""
|
||||||
|
variables:
|
||||||
|
download-button:
|
||||||
|
type: boolean
|
||||||
|
description: Download button
|
||||||
|
open-in-app-button:
|
||||||
|
type: boolean
|
||||||
|
description: Open in app button
|
||||||
|
pre-permission-notification-prompt:
|
||||||
|
description: A feature that shows the pre-permission notification prompt.
|
||||||
|
hasExposure: true
|
||||||
|
exposureDescription: ""
|
||||||
|
variables:
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
description: "if true, the pre-permission notification prompt is shown to the user."
|
||||||
|
print:
|
||||||
|
description: A feature for printing from the share or browser menu.
|
||||||
|
hasExposure: true
|
||||||
|
exposureDescription: ""
|
||||||
|
variables:
|
||||||
|
browser-print-enabled:
|
||||||
|
type: boolean
|
||||||
|
description: "If true, a print button from the browser menu is available."
|
||||||
|
share-print-enabled:
|
||||||
|
type: boolean
|
||||||
|
description: "If true, a print button from the share menu is available."
|
||||||
|
private-browsing:
|
||||||
|
description: Private Browsing Mode
|
||||||
|
hasExposure: true
|
||||||
|
exposureDescription: ""
|
||||||
|
variables:
|
||||||
|
felt-privacy-enabled:
|
||||||
|
type: boolean
|
||||||
|
description: "if true, enable felt privacy related UI"
|
||||||
|
re-engagement-notification:
|
||||||
|
description: A feature that shows the re-engagement notification if the user is inactive.
|
||||||
|
hasExposure: true
|
||||||
|
exposureDescription: ""
|
||||||
|
variables:
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
description: "If true, the re-engagement notification is shown to the inactive user."
|
||||||
|
type:
|
||||||
|
type: int
|
||||||
|
description: The type of re-engagement notification that is shown to the inactive user.
|
||||||
|
search-extra-params:
|
||||||
|
description: A feature that provides additional args for search.
|
||||||
|
hasExposure: true
|
||||||
|
exposureDescription: ""
|
||||||
|
variables:
|
||||||
|
channel-id:
|
||||||
|
type: json
|
||||||
|
description: The channel Id param name with arg.
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
description: "If true, the feature is active."
|
||||||
|
feature-enabler:
|
||||||
|
type: json
|
||||||
|
description: "The feature enabler param name with arg, NOTE this map could be empty."
|
||||||
|
search-engine:
|
||||||
|
type: string
|
||||||
|
description: The search engine name.
|
||||||
|
search-term-groups:
|
||||||
|
description: A feature allowing the grouping of URLs around the search term that it came from.
|
||||||
|
hasExposure: true
|
||||||
|
exposureDescription: ""
|
||||||
|
variables:
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
description: "If true, the feature shows up on the homescreen and on the new tab screen."
|
||||||
|
shopping-experience:
|
||||||
|
description: A feature that shows product review quality information.
|
||||||
|
hasExposure: true
|
||||||
|
exposureDescription: ""
|
||||||
|
variables:
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
description: "if true, the shopping experience feature is shown to the user."
|
||||||
|
product-recommendations:
|
||||||
|
type: boolean
|
||||||
|
description: "if true, recommended products feature is enabled to be shown to the user based on their preference."
|
||||||
|
splash-screen:
|
||||||
|
description: "A feature that extends splash screen duration, allowing additional data fetching time for the app's initial run."
|
||||||
|
hasExposure: true
|
||||||
|
exposureDescription: ""
|
||||||
|
variables:
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
description: "If true, the feature is active."
|
||||||
|
maximum_duration_ms:
|
||||||
|
type: int
|
||||||
|
description: The maximum amount of time in milliseconds the splashscreen will be visible while waiting for initialization calls to complete.
|
||||||
|
toolbar:
|
||||||
|
description: The searchbar/awesomebar that user uses to search.
|
||||||
|
hasExposure: true
|
||||||
|
exposureDescription: ""
|
||||||
|
variables:
|
||||||
|
toolbar-position-top:
|
||||||
|
type: boolean
|
||||||
|
description: "If true, toolbar appears at top of the screen."
|
||||||
|
unified-search:
|
||||||
|
description: A feature allowing user to easily search for specified results directly in the search bar.
|
||||||
|
hasExposure: true
|
||||||
|
exposureDescription: ""
|
||||||
|
variables:
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
description: "If true, the feature shows up in the search bar."
|
|
@ -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=org.mozilla.fenix.perf.SampleBenchmark
|
||||||
|
// - Use the IDE. Temporarily set the "benchmark" property in app/build.gradle with "ext.benchmark=true"
|
||||||
|
// near the top of the file. DO NOT COMMIT THIS.
|
||||||
|
// - (note: I was unable to get IDE run configurations working)
|
||||||
|
//
|
||||||
|
// To get the results, look at this file (we recommend using the median; results are in nanoseconds):
|
||||||
|
// app/build/outputs/connected_android_test_additional_output/nightlyAndroidTest/connected/<device>/org.mozilla.fenix-benchmarkData.json
|
||||||
|
//
|
||||||
|
// I was unable to get the results to print directly in Android Studio (perhaps it's my device).
|
||||||
|
//
|
||||||
|
// The official documentation suggests configuring microbenchmark in a separate module. This would
|
||||||
|
// require any benchmarked code to be in a library module, not the :app module (see below). To avoid
|
||||||
|
// this requirement, we created the "benchmark" gradle property.
|
||||||
|
//
|
||||||
|
// For the most accurate results, the documentation recommends running tests on rooted devices with
|
||||||
|
// the CPU clock locked.
|
||||||
|
//
|
||||||
|
// See https://developer.android.com/studio/profile/benchmark#what-to-benchmark for when writing a
|
||||||
|
// jetpack microbenchmark is a good fit.
|
||||||
|
|
||||||
|
// I think `android` represents this object:
|
||||||
|
// https://google.github.io/android-gradle-dsl/3.3/com.android.build.gradle.AppExtension.html
|
||||||
|
ext.maybeConfigForJetpackBenchmark = { android ->
|
||||||
|
if (!project.hasProperty("benchmark")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The official documentation https://developer.android.com/studio/profile/benchmark#full-setup
|
||||||
|
// recommends setting up the Microbenchmark library in a separate module from your app: AFAICT,
|
||||||
|
// the reason for this is to prevent the benchmarks from being configured against debug
|
||||||
|
// builds. We chose not to do this because it's a lot of work to pull code out into a
|
||||||
|
// separate module just to benchmark it. We were able to replicate the outcome by setting
|
||||||
|
// this testBuildType property.
|
||||||
|
android.testBuildType "nightly"
|
||||||
|
|
||||||
|
// WARNING: our proguard configuration for androidTest is not set up correctly so the tests
|
||||||
|
// fail if we don't disable minification. DISABLING MINIFICATION PRODUCES BENCHMARKS THAT ARE
|
||||||
|
// LESS REPRESENTATIVE TO THE USER EXPERIENCE, however, so we made this tradeoff to reduce
|
||||||
|
// implementation time.
|
||||||
|
project.ext.disableOptimization = true
|
||||||
|
|
||||||
|
android.defaultConfig {
|
||||||
|
// WARNING: the benchmark framework warns you if you're running the test in a configuration
|
||||||
|
// that will compromise the accuracy of the results. Unfortunately, I couldn't get everything
|
||||||
|
// working so I had to suppress some things.
|
||||||
|
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,913 @@
|
||||||
|
import com.android.build.api.variant.FilterConfiguration
|
||||||
|
import org.apache.tools.ant.util.StringUtils
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id "com.jetbrains.python.envs" version "$python_envs_plugin"
|
||||||
|
id "com.google.protobuf" version "$protobuf_plugin"
|
||||||
|
}
|
||||||
|
|
||||||
|
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 groovy.json.JsonOutput
|
||||||
|
import org.gradle.internal.logging.text.StyledTextOutput.Style
|
||||||
|
import org.gradle.internal.logging.text.StyledTextOutputFactory
|
||||||
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
|
||||||
|
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=beta ..)
|
||||||
|
// in order to run UI tests against other build variants than debug in automation.
|
||||||
|
testBuildType project.property("testBuildType")
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "com.leos"
|
||||||
|
minSdk = 26
|
||||||
|
compileSdk config.compileSdkVersion
|
||||||
|
targetSdkVersion config.targetSdkVersion
|
||||||
|
versionCode 1
|
||||||
|
versionName Config.generateDebugVersionName()
|
||||||
|
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", "\"Extensions-for-Android\""
|
||||||
|
buildConfigField "String", "AMO_COLLECTION_USER", "\"mozilla\""
|
||||||
|
// These add-ons should be excluded for Mozilla Online builds.
|
||||||
|
buildConfigField "String[]", "MOZILLA_ONLINE_ADDON_EXCLUSIONS",
|
||||||
|
"{" +
|
||||||
|
"\"uBlock0@raymondhill.net\"," +
|
||||||
|
"\"firefox@ghostery.com\"," +
|
||||||
|
"\"jid1-MnnxcxisBPnSXQ@jetpack\"," +
|
||||||
|
"\"adguardadblocker@adguard.com\"," +
|
||||||
|
"\"foxyproxy@eric.h.jung\"," +
|
||||||
|
"\"{73a6fe31-595d-460b-a920-fcc0f8843232}\"," +
|
||||||
|
"\"jid1-BoFifL9Vbdl2zQ@jetpack\"," +
|
||||||
|
"\"woop-NoopscooPsnSXQ@jetpack\"," +
|
||||||
|
"\"adnauseam@rednoise.org\"" +
|
||||||
|
"}"
|
||||||
|
// This should be the base URL used to call the AMO API.
|
||||||
|
buildConfigField "String", "AMO_SERVER_URL", "\"https://services.addons.mozilla.org\""
|
||||||
|
|
||||||
|
def deepLinkSchemeValue = "fenix-dev"
|
||||||
|
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
|
||||||
|
|
||||||
|
// This allows overriding the target activity for MozillaOnline builds, which happens
|
||||||
|
// as part of the defaultConfig below.
|
||||||
|
def targetActivity = "HomeActivity"
|
||||||
|
|
||||||
|
// Build flag for "Mozilla Online" variants. See `Config.isMozillaOnline`.
|
||||||
|
if (project.hasProperty("mozillaOnline") || gradle.hasProperty("localProperties.mozillaOnline")) {
|
||||||
|
buildConfigField "boolean", "MOZILLA_ONLINE", "true"
|
||||||
|
targetActivity = "MozillaOnlineHomeActivity"
|
||||||
|
} else {
|
||||||
|
buildConfigField "boolean", "MOZILLA_ONLINE", "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
manifestPlaceholders = [
|
||||||
|
"targetActivity": targetActivity,
|
||||||
|
"deepLinkScheme": deepLinkSchemeValue
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
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 ".fenix.debug"
|
||||||
|
resValue "bool", "IS_DEBUG", "true"
|
||||||
|
pseudoLocalesEnabled true
|
||||||
|
}
|
||||||
|
nightly releaseTemplate >> {
|
||||||
|
applicationIdSuffix ".fenix"
|
||||||
|
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
|
||||||
|
def deepLinkSchemeValue = "fenix-nightly"
|
||||||
|
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
|
||||||
|
manifestPlaceholders.putAll([
|
||||||
|
"deepLinkScheme": deepLinkSchemeValue
|
||||||
|
])
|
||||||
|
}
|
||||||
|
beta releaseTemplate >> {
|
||||||
|
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
|
||||||
|
applicationIdSuffix ".firefox_beta"
|
||||||
|
def deepLinkSchemeValue = "fenix-beta"
|
||||||
|
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
|
||||||
|
manifestPlaceholders.putAll([
|
||||||
|
// This release type is meant to replace Firefox (Beta channel) and therefore needs to inherit
|
||||||
|
// its sharedUserId for all eternity. See:
|
||||||
|
// https://searchfox.org/mozilla-esr68/search?q=moz_android_shared_id&case=false®exp=false&path=
|
||||||
|
// Shipping an app update without sharedUserId can have
|
||||||
|
// fatal consequences. For example see:
|
||||||
|
// - https://issuetracker.google.com/issues/36924841
|
||||||
|
// - https://issuetracker.google.com/issues/36905922
|
||||||
|
"sharedUserId": "org.mozilla.firefox.sharedID",
|
||||||
|
"deepLinkScheme": deepLinkSchemeValue,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
release releaseTemplate >> {
|
||||||
|
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
|
||||||
|
applicationIdSuffix ".firefox"
|
||||||
|
def deepLinkSchemeValue = "fenix"
|
||||||
|
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
|
||||||
|
manifestPlaceholders.putAll([
|
||||||
|
// This release type is meant to replace Firefox (Release channel) and therefore needs to inherit
|
||||||
|
// its sharedUserId for all eternity. See:
|
||||||
|
// https://searchfox.org/mozilla-esr68/search?q=moz_android_shared_id&case=false®exp=false&path=
|
||||||
|
// Shipping an app update without sharedUserId can have
|
||||||
|
// fatal consequences. For example see:
|
||||||
|
// - https://issuetracker.google.com/issues/36924841
|
||||||
|
// - https://issuetracker.google.com/issues/36905922
|
||||||
|
"sharedUserId": "org.mozilla.firefox.sharedID",
|
||||||
|
"deepLinkScheme": deepLinkSchemeValue,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
forkDebug {
|
||||||
|
shrinkResources false
|
||||||
|
minifyEnabled false
|
||||||
|
applicationIdSuffix ".leosium.debug"
|
||||||
|
pseudoLocalesEnabled true
|
||||||
|
// Need to replicate default debug config features
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
|
debuggable true
|
||||||
|
def deepLinkSchemeValue = "leosium-debug"
|
||||||
|
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
|
||||||
|
manifestPlaceholders.putAll([
|
||||||
|
"sharedUserId": "io.github.forkmaintainers.leosium.sharedID",
|
||||||
|
"deepLinkScheme": deepLinkSchemeValue,
|
||||||
|
])
|
||||||
|
// Use custom default allowed addon list
|
||||||
|
buildConfigField "String", "AMO_COLLECTION_USER", "\"16201230\""
|
||||||
|
buildConfigField "String", "AMO_COLLECTION_NAME", "\"What-I-want-on-Fenix\""
|
||||||
|
resValue "bool", "IS_DEBUG", "true"
|
||||||
|
matchingFallbacks = ['debug']
|
||||||
|
}
|
||||||
|
forkRelease releaseTemplate >> {
|
||||||
|
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
|
||||||
|
applicationIdSuffix ".leosium"
|
||||||
|
def deepLinkSchemeValue = "leosium"
|
||||||
|
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
|
||||||
|
manifestPlaceholders.putAll([
|
||||||
|
"sharedUserId": "io.github.forkmaintainers.leosium.sharedID",
|
||||||
|
"deepLinkScheme": deepLinkSchemeValue,
|
||||||
|
])
|
||||||
|
// Use custom default allowed addon list
|
||||||
|
buildConfigField "String", "AMO_COLLECTION_USER", "\"16201230\""
|
||||||
|
buildConfigField "String", "AMO_COLLECTION_NAME", "\"What-I-want-on-Fenix\""
|
||||||
|
}
|
||||||
|
benchmark releaseTemplate >> {
|
||||||
|
initWith buildTypes.nightly
|
||||||
|
applicationIdSuffix ".fenix"
|
||||||
|
debuggable false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding 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.add("product")
|
||||||
|
|
||||||
|
productFlavors {
|
||||||
|
fenix {
|
||||||
|
dimension "product"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
bundle {
|
||||||
|
language {
|
||||||
|
// Because we have runtime language selection we will keep all strings and languages
|
||||||
|
// in the base APKs.
|
||||||
|
enableSplit = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
'META-INF/LICENSE.md', 'META-INF/LICENSE-notice.md']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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.compose_compiler
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace 'org.mozilla.fenix'
|
||||||
|
}
|
||||||
|
|
||||||
|
android.applicationVariants.configureEach { variant ->
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
// Generate version codes for builds
|
||||||
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def isDebug = variant.buildType.resValues['bool/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)
|
||||||
|
println("Telemetry enabled: " + !isDebug)
|
||||||
|
|
||||||
|
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 = variant.buildType.name == 'nightly' ? Config.nightlyVersionName() : Config.releaseVersionName(project)
|
||||||
|
// println("versionName override: $versionName")
|
||||||
|
|
||||||
|
variant.outputs.each { output ->
|
||||||
|
def isMozillaOnline = project.hasProperty("mozillaOnline") || gradle.hasProperty("localProperties.mozillaOnline")
|
||||||
|
def abi = output.getFilter(FilterConfiguration.FilterType.ABI.name())
|
||||||
|
// If it is a Mozilla Online build, use a unified version code of armeabi-v7a
|
||||||
|
def arch = (isMozillaOnline) ? "armeabi-v7a" : abi
|
||||||
|
def aab = project.hasProperty("aab")
|
||||||
|
// 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(arch, aab)
|
||||||
|
|
||||||
|
println("versionCode for $abi = $versionCodeOverride, isMozillaOnline = $isMozillaOnline")
|
||||||
|
|
||||||
|
if (versionName != null) {
|
||||||
|
output.versionNameOverride = versionName
|
||||||
|
}
|
||||||
|
output.versionCodeOverride = versionCodeOverride
|
||||||
|
}
|
||||||
|
} else if (gradle.hasProperty("localProperties.branchBuild.fenix.version")) {
|
||||||
|
def versionName = gradle.getProperty("localProperties.branchBuild.fenix.version")
|
||||||
|
println("versionName override: $versionName")
|
||||||
|
variant.outputs.each { output ->
|
||||||
|
output.versionNameOverride = versionName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
// BuildConfig: Set variables for Sentry, Crash Reporting, and Telemetry
|
||||||
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
buildConfigField 'String', 'SENTRY_TOKEN', 'null'
|
||||||
|
if (!isDebug) {
|
||||||
|
buildConfigField 'boolean', 'CRASH_REPORTING', 'false'
|
||||||
|
// Reading sentry token from local file (if it exists). In a release task on taskcluster 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'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDebug) {
|
||||||
|
buildConfigField 'boolean', 'TELEMETRY', 'false'
|
||||||
|
} else {
|
||||||
|
buildConfigField 'boolean', 'TELEMETRY', '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 + '"'
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
// Adjust: Read token from local file if it exists (Only release builds)
|
||||||
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
print("Adjust token: ")
|
||||||
|
|
||||||
|
if (!isDebug) {
|
||||||
|
try {
|
||||||
|
def token = new File("${rootDir}/.adjust_token").text.trim()
|
||||||
|
buildConfigField 'String', 'ADJUST_TOKEN', '"' + token + '"'
|
||||||
|
println "(Added from .adjust_token file)"
|
||||||
|
} catch (FileNotFoundException ignored) {
|
||||||
|
buildConfigField 'String', 'ADJUST_TOKEN', 'null'
|
||||||
|
println("X_X")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buildConfigField 'String', 'ADJUST_TOKEN', 'null'
|
||||||
|
println("--")
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
// Nimbus: Read endpoint from local.properties of a local file if it exists
|
||||||
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
print("Nimbus endpoint: ")
|
||||||
|
|
||||||
|
if (!isDebug) {
|
||||||
|
try {
|
||||||
|
def url = new File("${rootDir}/.nimbus").text.trim()
|
||||||
|
buildConfigField 'String', 'NIMBUS_ENDPOINT', '"' + url + '"'
|
||||||
|
println "(Added from .nimbus file)"
|
||||||
|
} catch (FileNotFoundException ignored) {
|
||||||
|
buildConfigField 'String', 'NIMBUS_ENDPOINT', 'null'
|
||||||
|
println("X_X")
|
||||||
|
}
|
||||||
|
} else if (gradle.hasProperty("localProperties.nimbus.remote-settings.url")) {
|
||||||
|
def url=gradle.getProperty("localProperties.nimbus.remote-settings.url")
|
||||||
|
buildConfigField 'String', 'NIMBUS_ENDPOINT', '"' + url + '"'
|
||||||
|
println "(Added from local.properties file)"
|
||||||
|
} else {
|
||||||
|
buildConfigField 'String', 'NIMBUS_ENDPOINT', 'null'
|
||||||
|
println("--")
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
// Glean: Read custom server URL from local.properties of a local file if it exists
|
||||||
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
print("Glean custom server URL: ")
|
||||||
|
|
||||||
|
if (gradle.hasProperty("localProperties.glean.custom.server.url")) {
|
||||||
|
def url=gradle.getProperty("localProperties.glean.custom.server.url")
|
||||||
|
buildConfigField 'String', 'GLEAN_CUSTOM_URL', url
|
||||||
|
println "(Added from local.properties file)"
|
||||||
|
} else {
|
||||||
|
buildConfigField 'String', 'GLEAN_CUSTOM_URL', 'null'
|
||||||
|
println("--")
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
// 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()
|
||||||
|
def token = "https://assets.mozilla.net/mobile-wallpapers/android"
|
||||||
|
buildConfigField 'String', 'WALLPAPER_URL', '"' + token + '"'
|
||||||
|
println "(Added from .wallpaper_url file)"
|
||||||
|
} catch (FileNotFoundException ignored) {
|
||||||
|
buildConfigField 'String', 'WALLPAPER_URL', '""'
|
||||||
|
println("--")
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
// BuildConfig: Set the Pocket consumer key from a local file if it exists
|
||||||
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
print("Pocket consumer key: ")
|
||||||
|
|
||||||
|
try {
|
||||||
|
def token = new File("${rootDir}/.pocket_consumer_key").text.trim()
|
||||||
|
buildConfigField 'String', 'POCKET_CONSUMER_KEY', '"' + token + '"'
|
||||||
|
println "(Added from .pocket_consumer_key file)"
|
||||||
|
} catch (FileNotFoundException ignored) {
|
||||||
|
buildConfigField 'String', 'POCKET_CONSUMER_KEY', '""'
|
||||||
|
println("--")
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
// BuildConfig: Set flag to disable LeakCanary in debug (on CI builds)
|
||||||
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
if (project.hasProperty("disableLeakCanary") || gradle.hasProperty("localProperties.disableLeakCanary")) {
|
||||||
|
buildConfigField "boolean", "LEAKCANARY", "false"
|
||||||
|
println("LeakCanary enabled in debug: false")
|
||||||
|
} else {
|
||||||
|
buildConfigField "boolean", "LEAKCANARY", "true"
|
||||||
|
println("LeakCanary enabled in debug: true")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buildConfigField "boolean", "LEAKCANARY", "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate Kotlin code for the Fenix Glean metrics.
|
||||||
|
apply plugin: "org.mozilla.telemetry.glean-gradle-plugin"
|
||||||
|
apply plugin: "org.mozilla.appservices.nimbus-gradle-plugin"
|
||||||
|
|
||||||
|
nimbus {
|
||||||
|
// The path to the Nimbus feature manifest file
|
||||||
|
manifestFile = "nimbus.fml.yaml"
|
||||||
|
// The fully qualified class name for the generated features.
|
||||||
|
// Map from the variant name to the channel as experimenter and nimbus understand it.
|
||||||
|
// If nimbus's channels were accurately set up well for this project, then this
|
||||||
|
// shouldn't be needed.
|
||||||
|
channels = [
|
||||||
|
fenixDebug: "developer",
|
||||||
|
fenixNightly: "nightly",
|
||||||
|
fenixBeta: "beta",
|
||||||
|
fenixRelease: "release",
|
||||||
|
fenixForkDebug: "forkDebug",
|
||||||
|
fenixForkRelease: "forkRelease",
|
||||||
|
fenixBenchmark: "developer"
|
||||||
|
]
|
||||||
|
// This is generated by the FML and should be checked into git.
|
||||||
|
// It will be fetched by Experimenter (the Nimbus experiment website)
|
||||||
|
// and used to inform experiment configuration.
|
||||||
|
experimenterManifest = ".experimenter.yaml"
|
||||||
|
applicationServicesDir = gradle.hasProperty('localProperties.autoPublish.application-services.dir')
|
||||||
|
? gradle.getProperty('localProperties.autoPublish.application-services.dir') : null
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(KotlinCompile).configureEach {
|
||||||
|
kotlinOptions.freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(':browser-engine-gecko')
|
||||||
|
|
||||||
|
implementation ComponentsDependencies.kotlin_coroutines
|
||||||
|
testImplementation ComponentsDependencies.testing_coroutines
|
||||||
|
implementation ComponentsDependencies.androidx_appcompat
|
||||||
|
implementation ComponentsDependencies.androidx_constraintlayout
|
||||||
|
implementation ComponentsDependencies.androidx_coordinatorlayout
|
||||||
|
implementation FenixDependencies.google_accompanist_drawablepainter
|
||||||
|
|
||||||
|
implementation ComponentsDependencies.thirdparty_sentry_latest
|
||||||
|
|
||||||
|
implementation project(':compose-awesomebar')
|
||||||
|
implementation project(':compose-cfr')
|
||||||
|
|
||||||
|
implementation project(':concept-awesomebar')
|
||||||
|
implementation project(':concept-base')
|
||||||
|
implementation project(':concept-engine')
|
||||||
|
implementation project(':concept-menu')
|
||||||
|
implementation project(':concept-push')
|
||||||
|
implementation project(':concept-storage')
|
||||||
|
implementation project(':concept-sync')
|
||||||
|
implementation project(':concept-toolbar')
|
||||||
|
implementation project(':concept-tabstray')
|
||||||
|
|
||||||
|
implementation project(':browser-domains')
|
||||||
|
implementation project(':browser-icons')
|
||||||
|
implementation project(':browser-menu')
|
||||||
|
implementation project(':browser-menu2')
|
||||||
|
implementation project(':browser-session-storage')
|
||||||
|
implementation project(':browser-state')
|
||||||
|
implementation project(':browser-storage-sync')
|
||||||
|
implementation project(':browser-tabstray')
|
||||||
|
implementation project(':browser-thumbnails')
|
||||||
|
implementation project(':browser-toolbar')
|
||||||
|
|
||||||
|
implementation project(':feature-addons')
|
||||||
|
implementation project(':feature-accounts')
|
||||||
|
implementation project(':feature-app-links')
|
||||||
|
implementation project(':feature-autofill')
|
||||||
|
implementation project(':feature-awesomebar')
|
||||||
|
implementation project(':feature-contextmenu')
|
||||||
|
implementation project(':feature-customtabs')
|
||||||
|
implementation project(':feature-downloads')
|
||||||
|
implementation project(':feature-fxsuggest')
|
||||||
|
implementation project(':feature-intent')
|
||||||
|
implementation project(':feature-media')
|
||||||
|
implementation project(':feature-prompts')
|
||||||
|
implementation project(':feature-push')
|
||||||
|
implementation project(':feature-privatemode')
|
||||||
|
implementation project(':feature-pwa')
|
||||||
|
implementation project(':feature-qr')
|
||||||
|
implementation project(':feature-search')
|
||||||
|
implementation project(':feature-session')
|
||||||
|
implementation project(':feature-syncedtabs')
|
||||||
|
implementation project(':feature-toolbar')
|
||||||
|
implementation project(':feature-tabs')
|
||||||
|
implementation project(':feature-findinpage')
|
||||||
|
implementation project(':feature-logins')
|
||||||
|
implementation project(':feature-sitepermissions')
|
||||||
|
implementation project(':feature-readerview')
|
||||||
|
implementation project(':feature-tab-collections')
|
||||||
|
implementation project(':feature-recentlyclosed')
|
||||||
|
implementation project(':feature-top-sites')
|
||||||
|
implementation project(':feature-share')
|
||||||
|
implementation project(':feature-accounts-push')
|
||||||
|
implementation project(':feature-webauthn')
|
||||||
|
implementation project(':feature-webcompat')
|
||||||
|
implementation project(':feature-webnotifications')
|
||||||
|
implementation project(':feature-webcompat-reporter')
|
||||||
|
|
||||||
|
implementation project(':service-pocket')
|
||||||
|
implementation project(':service-contile')
|
||||||
|
implementation project(':service-digitalassetlinks')
|
||||||
|
implementation project(':service-sync-autofill')
|
||||||
|
implementation project(':service-sync-logins')
|
||||||
|
implementation project(':service-firefox-accounts')
|
||||||
|
implementation project(':service-glean')
|
||||||
|
implementation project(':service-location')
|
||||||
|
implementation project(':service-nimbus')
|
||||||
|
|
||||||
|
implementation project(':support-webextensions')
|
||||||
|
implementation project(':support-base')
|
||||||
|
implementation project(':support-rusterrors')
|
||||||
|
implementation project(':support-images')
|
||||||
|
implementation project(':support-ktx')
|
||||||
|
implementation project(':support-rustlog')
|
||||||
|
implementation project(':support-utils')
|
||||||
|
implementation project(':support-locale')
|
||||||
|
|
||||||
|
implementation project(':ui-colors')
|
||||||
|
implementation project(':ui-icons')
|
||||||
|
implementation project(':lib-publicsuffixlist')
|
||||||
|
implementation project(':ui-widgets')
|
||||||
|
implementation project(':ui-tabcounter')
|
||||||
|
|
||||||
|
implementation project(':lib-crash')
|
||||||
|
implementation project(':lib-crash-sentry')
|
||||||
|
implementation project(':lib-state')
|
||||||
|
implementation project(':lib-dataprotect')
|
||||||
|
|
||||||
|
debugImplementation ComponentsDependencies.leakcanary
|
||||||
|
forkDebugImplementation ComponentsDependencies.leakcanary
|
||||||
|
debugImplementation ComponentsDependencies.androidx_compose_ui_tooling
|
||||||
|
|
||||||
|
implementation ComponentsDependencies.androidx_activity_compose
|
||||||
|
implementation FenixDependencies.androidx_activity_ktx
|
||||||
|
implementation ComponentsDependencies.androidx_annotation
|
||||||
|
implementation ComponentsDependencies.androidx_compose_ui
|
||||||
|
implementation ComponentsDependencies.androidx_compose_ui_tooling_preview
|
||||||
|
implementation ComponentsDependencies.androidx_compose_animation
|
||||||
|
implementation ComponentsDependencies.androidx_compose_foundation
|
||||||
|
implementation ComponentsDependencies.androidx_compose_material
|
||||||
|
implementation FenixDependencies.androidx_legacy
|
||||||
|
implementation ComponentsDependencies.androidx_biometric
|
||||||
|
implementation ComponentsDependencies.androidx_paging
|
||||||
|
implementation ComponentsDependencies.androidx_preferences
|
||||||
|
implementation ComponentsDependencies.androidx_fragment
|
||||||
|
implementation FenixDependencies.androidx_navigation_fragment
|
||||||
|
implementation FenixDependencies.androidx_navigation_ui
|
||||||
|
implementation ComponentsDependencies.androidx_recyclerview
|
||||||
|
implementation ComponentsDependencies.androidx_lifecycle_common
|
||||||
|
implementation ComponentsDependencies.androidx_lifecycle_livedata
|
||||||
|
implementation ComponentsDependencies.androidx_lifecycle_process
|
||||||
|
implementation ComponentsDependencies.androidx_lifecycle_runtime
|
||||||
|
|
||||||
|
implementation ComponentsDependencies.androidx_lifecycle_viewmodel
|
||||||
|
implementation ComponentsDependencies.androidx_core
|
||||||
|
implementation ComponentsDependencies.androidx_core_ktx
|
||||||
|
implementation FenixDependencies.androidx_core_splashscreen
|
||||||
|
implementation FenixDependencies.androidx_transition
|
||||||
|
implementation ComponentsDependencies.androidx_work_runtime
|
||||||
|
implementation FenixDependencies.androidx_datastore
|
||||||
|
implementation ComponentsDependencies.androidx_data_store_preferences
|
||||||
|
implementation FenixDependencies.protobuf_javalite
|
||||||
|
implementation ComponentsDependencies.google_material
|
||||||
|
|
||||||
|
androidTestImplementation ComponentsDependencies.androidx_test_uiautomator
|
||||||
|
androidTestImplementation FenixDependencies.fastlane
|
||||||
|
// This Falcon version is added to maven central now required for Screengrab
|
||||||
|
androidTestImplementation FenixDependencies.falcon
|
||||||
|
|
||||||
|
androidTestImplementation ComponentsDependencies.androidx_compose_ui_test
|
||||||
|
|
||||||
|
androidTestImplementation ComponentsDependencies.androidx_espresso_core, {
|
||||||
|
exclude group: 'com.android.support', module: 'support-annotations'
|
||||||
|
}
|
||||||
|
|
||||||
|
androidTestImplementation(FenixDependencies.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 ComponentsDependencies.androidx_test_core
|
||||||
|
androidTestImplementation FenixDependencies.espresso_idling_resources
|
||||||
|
androidTestImplementation FenixDependencies.espresso_intents
|
||||||
|
|
||||||
|
androidTestImplementation ComponentsDependencies.androidx_test_runner
|
||||||
|
androidTestImplementation ComponentsDependencies.androidx_test_rules
|
||||||
|
androidTestUtil FenixDependencies.orchestrator
|
||||||
|
androidTestImplementation ComponentsDependencies.androidx_espresso_core, {
|
||||||
|
exclude group: 'com.android.support', module: 'support-annotations'
|
||||||
|
}
|
||||||
|
|
||||||
|
androidTestImplementation ComponentsDependencies.androidx_test_junit
|
||||||
|
androidTestImplementation ComponentsDependencies.androidx_work_testing
|
||||||
|
androidTestImplementation FenixDependencies.androidx_benchmark_junit4
|
||||||
|
androidTestImplementation ComponentsDependencies.testing_mockwebserver
|
||||||
|
testImplementation project(':support-test')
|
||||||
|
testImplementation project(':support-test-libstate')
|
||||||
|
testImplementation ComponentsDependencies.androidx_test_junit
|
||||||
|
testImplementation ComponentsDependencies.androidx_work_testing
|
||||||
|
testImplementation (ComponentsDependencies.testing_robolectric) {
|
||||||
|
exclude group: 'org.apache.maven'
|
||||||
|
}
|
||||||
|
|
||||||
|
testImplementation ComponentsDependencies.testing_maven_ant_tasks
|
||||||
|
implementation project(':support-rusthttp')
|
||||||
|
|
||||||
|
androidTestImplementation FenixDependencies.mockk_android
|
||||||
|
testImplementation FenixDependencies.mockk
|
||||||
|
|
||||||
|
// For the initial release of Glean 19, we require consumer applications to
|
||||||
|
// depend on a separate library for unit tests. This will be removed in future releases.
|
||||||
|
testImplementation "org.mozilla.telemetry:glean-native-forUnitTests:${project.ext.glean_version}"
|
||||||
|
|
||||||
|
lintChecks project(":mozilla-lint-rules")
|
||||||
|
}
|
||||||
|
|
||||||
|
protobuf {
|
||||||
|
protoc {
|
||||||
|
artifact = FenixDependencies.protobuf_compiler
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates the java Protobuf-lite code for the Protobufs in this project. See
|
||||||
|
// https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
|
||||||
|
// for more information.
|
||||||
|
generateProtoTasks {
|
||||||
|
all().each { task ->
|
||||||
|
task.builtins {
|
||||||
|
java {
|
||||||
|
option 'lite'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (project.hasProperty("coverage")) {
|
||||||
|
tasks.withType(Test).configureEach {
|
||||||
|
jacoco.includeNoLocationClasses = true
|
||||||
|
jacoco.excludes = ['jdk.internal.*']
|
||||||
|
}
|
||||||
|
|
||||||
|
jacoco {
|
||||||
|
toolVersion = Versions.jacoco
|
||||||
|
}
|
||||||
|
|
||||||
|
android.applicationVariants.configureEach { variant ->
|
||||||
|
tasks.register("jacoco${variant.name.capitalize()}TestReport", JacocoReport) {
|
||||||
|
dependsOn "test${variant.name.capitalize()}UnitTest"
|
||||||
|
|
||||||
|
reports {
|
||||||
|
xml.required = true
|
||||||
|
html.required = 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
|
||||||
|
}
|
||||||
|
forkDebug {
|
||||||
|
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(FilterConfiguration.FilterType.ABI.name()),
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register('buildTranslationArray') {
|
||||||
|
// 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(',}', '}')
|
||||||
|
android.defaultConfig.buildConfigField "String[]", "SUPPORTED_LOCALE_ARRAY", foundLocalesString
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = StringUtils.removeSuffix(gradle."localProperties.dependencySubstitutions.geckoviewTopsrcdir", File.separator)
|
||||||
|
apply from: "${topsrcdir}/substitute-local-geckoview.gradle"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gradle.hasProperty('localProperties.autoPublish.glean.dir')) {
|
||||||
|
ext.gleanSrcDir = gradle."localProperties.autoPublish.glean.dir"
|
||||||
|
apply from: "../${gleanSrcDir}/build-scripts/substitute-local-glean.gradle"
|
||||||
|
}
|
||||||
|
|
||||||
|
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()}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable expiration by major version.
|
||||||
|
ext.gleanExpireByVersion = 25
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,70 @@
|
||||||
|
<?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" />
|
||||||
|
|
||||||
|
<!-- 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="VectorPath" 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 -->
|
||||||
|
|
||||||
|
<!-- Correctness: checks with increased severity -->
|
||||||
|
<issue id="PrivateResource" severity="error" />
|
||||||
|
</lint>
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,483 @@
|
||||||
|
---
|
||||||
|
about:
|
||||||
|
description: Nimbus Feature Manifest for Fenix (Firefox Android)
|
||||||
|
kotlin:
|
||||||
|
package: org.mozilla.fenix
|
||||||
|
class: .nimbus.FxNimbus
|
||||||
|
channels:
|
||||||
|
- release
|
||||||
|
- beta
|
||||||
|
- nightly
|
||||||
|
- developer
|
||||||
|
- forkDebug
|
||||||
|
- forkRelease
|
||||||
|
includes:
|
||||||
|
- onboarding.fml.yaml
|
||||||
|
- pbm.fml.yaml
|
||||||
|
import:
|
||||||
|
- path: ../android-components/components/service/nimbus/messaging.fml.yaml
|
||||||
|
channel: release
|
||||||
|
features:
|
||||||
|
messaging:
|
||||||
|
- value:
|
||||||
|
triggers:
|
||||||
|
# Using attributes built into the Nimbus SDK
|
||||||
|
USER_RECENTLY_INSTALLED: days_since_install < 7
|
||||||
|
USER_RECENTLY_UPDATED: days_since_update < 7 && days_since_install != days_since_update
|
||||||
|
USER_TIER_ONE_COUNTRY: ('US' in locale || 'GB' in locale || 'CA' in locale || 'DE' in locale || 'FR' in locale)
|
||||||
|
USER_EN_SPEAKER: "'en' in locale"
|
||||||
|
USER_ES_SPEAKER: "'es' in locale"
|
||||||
|
USER_DE_SPEAKER: "'de' in locale"
|
||||||
|
USER_FR_SPEAKER: "'fr' in locale"
|
||||||
|
DEVICE_ANDROID: os == 'Android'
|
||||||
|
DEVICE_IOS: os == 'iOS'
|
||||||
|
ALWAYS: "true"
|
||||||
|
NEVER: "false"
|
||||||
|
DAY_1_AFTER_INSTALL: days_since_install == 1
|
||||||
|
DAY_2_AFTER_INSTALL: days_since_install == 2
|
||||||
|
DAY_3_AFTER_INSTALL: days_since_install == 3
|
||||||
|
DAY_4_AFTER_INSTALL: days_since_install == 4
|
||||||
|
DAY_5_AFTER_INSTALL: days_since_install == 5
|
||||||
|
MORE_THAN_24H_SINCE_INSTALLED_OR_UPDATED: days_since_update >= 1
|
||||||
|
|
||||||
|
# Using custom attributes for the browser
|
||||||
|
I_AM_DEFAULT_BROWSER: "is_default_browser"
|
||||||
|
I_AM_NOT_DEFAULT_BROWSER: "is_default_browser == false"
|
||||||
|
USER_ESTABLISHED_INSTALL: "number_of_app_launches >=4"
|
||||||
|
|
||||||
|
FUNNEL_PAID: "adjust_campaign != ''"
|
||||||
|
FUNNEL_ORGANIC: "adjust_campaign == ''"
|
||||||
|
|
||||||
|
# Using Glean events, specific to the browser
|
||||||
|
INACTIVE_1_DAY: "'app_launched'|eventLastSeen('Hours') >= 24"
|
||||||
|
INACTIVE_2_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 2"
|
||||||
|
INACTIVE_3_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 3"
|
||||||
|
INACTIVE_4_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 4"
|
||||||
|
INACTIVE_5_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 5"
|
||||||
|
|
||||||
|
# Has the user signed in the last 4 years
|
||||||
|
FXA_SIGNED_IN: "'sync_auth.sign_in'|eventLastSeen('Years', 0) <= 4"
|
||||||
|
FXA_NOT_SIGNED_IN: "'sync_auth.sign_in'|eventLastSeen('Years', 0) > 4"
|
||||||
|
|
||||||
|
# https://mozilla-hub.atlassian.net/wiki/spaces/FJT/pages/11469471/Core+Active
|
||||||
|
USER_INFREQUENT: "'app_launched'|eventCountNonZero('Days', 28) >= 1 && 'app_launched'|eventCountNonZero('Days', 28) < 7"
|
||||||
|
USER_CASUAL: "'app_launched'|eventCountNonZero('Days', 28) >= 7 && 'app_launched'|eventCountNonZero('Days', 28) < 14"
|
||||||
|
USER_REGULAR: "'app_launched'|eventCountNonZero('Days', 28) >= 14 && 'app_launched'|eventCountNonZero('Days', 28) < 21"
|
||||||
|
USER_CORE_ACTIVE: "'app_launched'|eventCountNonZero('Days', 28) >= 21"
|
||||||
|
|
||||||
|
LAUNCHED_ONCE_THIS_WEEK: "'app_launched'|eventSum('Days', 7) == 1"
|
||||||
|
|
||||||
|
actions:
|
||||||
|
ENABLE_PRIVATE_BROWSING: ://enable_private_browsing
|
||||||
|
INSTALL_SEARCH_WIDGET: ://install_search_widget
|
||||||
|
MAKE_DEFAULT_BROWSER: ://make_default_browser
|
||||||
|
VIEW_BOOKMARKS: ://urls_bookmarks
|
||||||
|
VIEW_COLLECTIONS: ://home_collections
|
||||||
|
VIEW_HISTORY: ://urls_history
|
||||||
|
VIEW_HOMESCREEN: ://home
|
||||||
|
OPEN_SETTINGS_ACCESSIBILITY: ://settings_accessibility
|
||||||
|
OPEN_SETTINGS_ADDON_MANAGER: ://settings_addon_manager
|
||||||
|
OPEN_SETTINGS_DELETE_BROWSING_DATA: ://settings_delete_browsing_data
|
||||||
|
OPEN_SETTINGS_LOGINS: ://settings_logins
|
||||||
|
OPEN_SETTINGS_NOTIFICATIONS: ://settings_notifications
|
||||||
|
OPEN_SETTINGS_PRIVACY: ://settings_privacy
|
||||||
|
OPEN_SETTINGS_SEARCH_ENGINE: ://settings_search_engine
|
||||||
|
OPEN_SETTINGS_TRACKING_PROTECTION: ://settings_tracking_protection
|
||||||
|
OPEN_SETTINGS_WALLPAPERS: ://settings_wallpapers
|
||||||
|
OPEN_SETTINGS: ://settings
|
||||||
|
TURN_ON_SYNC: ://turn_on_sync
|
||||||
|
styles:
|
||||||
|
DEFAULT:
|
||||||
|
priority: 50
|
||||||
|
max-display-count: 5
|
||||||
|
SURVEY:
|
||||||
|
priority: 55
|
||||||
|
max-display-count: 1
|
||||||
|
PERSISTENT:
|
||||||
|
priority: 50
|
||||||
|
max-display-count: 20
|
||||||
|
WARNING:
|
||||||
|
priority: 60
|
||||||
|
max-display-count: 10
|
||||||
|
URGENT:
|
||||||
|
priority: 100
|
||||||
|
max-display-count: 10
|
||||||
|
NOTIFICATION:
|
||||||
|
priority: 50
|
||||||
|
max-display-count: 1
|
||||||
|
messages:
|
||||||
|
default-browser:
|
||||||
|
text: default_browser_experiment_card_text
|
||||||
|
surface: homescreen
|
||||||
|
action: "MAKE_DEFAULT_BROWSER"
|
||||||
|
trigger: [ "I_AM_NOT_DEFAULT_BROWSER","USER_ESTABLISHED_INSTALL" ]
|
||||||
|
style: PERSISTENT
|
||||||
|
button-label: preferences_set_as_default_browser
|
||||||
|
default-browser-notification:
|
||||||
|
title: nimbus_notification_default_browser_title
|
||||||
|
text: nimbus_notification_default_browser_text
|
||||||
|
surface: notification
|
||||||
|
style: NOTIFICATION
|
||||||
|
trigger:
|
||||||
|
- I_AM_NOT_DEFAULT_BROWSER
|
||||||
|
- DAY_3_AFTER_INSTALL
|
||||||
|
action: MAKE_DEFAULT_BROWSER
|
||||||
|
|
||||||
|
- channel: developer
|
||||||
|
value:
|
||||||
|
styles:
|
||||||
|
DEFAULT:
|
||||||
|
priority: 50
|
||||||
|
max-display-count: 100
|
||||||
|
EXPIRES_QUICKLY:
|
||||||
|
priority: 100
|
||||||
|
max-display-count: 1
|
||||||
|
notification-config:
|
||||||
|
refresh-interval: 120 # minutes (2 hours)
|
||||||
|
- path: ../android-components/components/browser/engine-gecko/geckoview.fml.yaml
|
||||||
|
channel: release
|
||||||
|
features:
|
||||||
|
pdfjs:
|
||||||
|
- channel: developer
|
||||||
|
value: {
|
||||||
|
download-button: true,
|
||||||
|
open-in-app-button: true
|
||||||
|
}
|
||||||
|
|
||||||
|
features:
|
||||||
|
toolbar:
|
||||||
|
description: The searchbar/awesomebar that user uses to search.
|
||||||
|
variables:
|
||||||
|
toolbar-position-top:
|
||||||
|
description: If true, toolbar appears at top of the screen.
|
||||||
|
type: Boolean
|
||||||
|
default: false
|
||||||
|
homescreen:
|
||||||
|
description: The homescreen that the user goes to when they press home or new tab.
|
||||||
|
variables:
|
||||||
|
sections-enabled:
|
||||||
|
description: "This property provides a lookup table of whether or not the given section should be enabled.
|
||||||
|
If the section is enabled, it should be toggleable in the settings screen, and on by default."
|
||||||
|
type: Map<HomeScreenSection, Boolean>
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
"top-sites": true,
|
||||||
|
"jump-back-in": true,
|
||||||
|
"recently-saved": true,
|
||||||
|
"recent-explorations": true,
|
||||||
|
"pocket": true,
|
||||||
|
"pocket-sponsored-stories": true,
|
||||||
|
}
|
||||||
|
defaults:
|
||||||
|
- channel: nightly
|
||||||
|
value: {
|
||||||
|
"sections-enabled": {
|
||||||
|
"top-sites": true,
|
||||||
|
"jump-back-in": true,
|
||||||
|
"recently-saved": true,
|
||||||
|
"recent-explorations": true,
|
||||||
|
"pocket": true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nimbus-validation:
|
||||||
|
description: "A feature that does not correspond to an application feature suitable for showing
|
||||||
|
that Nimbus is working. This should never be used in production."
|
||||||
|
variables:
|
||||||
|
settings-title:
|
||||||
|
description: The title of displayed in the Settings screen and app menu.
|
||||||
|
type: Text
|
||||||
|
default: browser_menu_settings
|
||||||
|
settings-punctuation:
|
||||||
|
description: The emoji displayed in the Settings screen title.
|
||||||
|
type: String
|
||||||
|
default: ""
|
||||||
|
settings-icon:
|
||||||
|
description: The drawable displayed in the app menu for Settings
|
||||||
|
type: String
|
||||||
|
default: mozac_ic_settings
|
||||||
|
search-term-groups:
|
||||||
|
description: A feature allowing the grouping of URLs around the search term that it came from.
|
||||||
|
variables:
|
||||||
|
enabled:
|
||||||
|
description: If true, the feature shows up on the homescreen and on the new tab screen.
|
||||||
|
type: Boolean
|
||||||
|
default: false
|
||||||
|
defaults:
|
||||||
|
- channel: nightly
|
||||||
|
value:
|
||||||
|
enabled: true
|
||||||
|
- channel: developer
|
||||||
|
value:
|
||||||
|
enabled: true
|
||||||
|
mr2022:
|
||||||
|
description: Features for MR 2022.
|
||||||
|
variables:
|
||||||
|
sections-enabled:
|
||||||
|
description: "This property provides a lookup table of whether or not the given section should be enabled."
|
||||||
|
type: Map<MR2022Section, Boolean>
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
"home-onboarding-dialog-existing-users": true,
|
||||||
|
"sync-cfr": true,
|
||||||
|
"wallpapers-selection-tool": true,
|
||||||
|
"jump-back-in-cfr": true,
|
||||||
|
"tcp-cfr": true,
|
||||||
|
"tcp-feature": true,
|
||||||
|
}
|
||||||
|
defaults:
|
||||||
|
- channel: developer
|
||||||
|
value: {
|
||||||
|
"sections-enabled": {
|
||||||
|
"home-onboarding-dialog-existing-users": true,
|
||||||
|
"sync-cfr": true,
|
||||||
|
"wallpapers-selection-tool": true,
|
||||||
|
"jump-back-in-cfr": true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cookie-banners:
|
||||||
|
description: Features for cookie banner handling.
|
||||||
|
variables:
|
||||||
|
sections-enabled:
|
||||||
|
description: "This property provides a lookup table of whether or not the given section should be enabled."
|
||||||
|
type: Map<CookieBannersSection, Int>
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
"feature-ui": 0,
|
||||||
|
"feature-setting-value": 0,
|
||||||
|
"dialog-re-engage-time": 4,
|
||||||
|
"feature-setting-value-pbm": 0
|
||||||
|
}
|
||||||
|
defaults:
|
||||||
|
- channel: developer
|
||||||
|
value: {
|
||||||
|
"sections-enabled": {
|
||||||
|
"feature-ui": 1,
|
||||||
|
"feature-setting-value": 0,
|
||||||
|
"dialog-re-engage-time": 4,
|
||||||
|
"feature-setting-value-pbm": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
- channel: nightly
|
||||||
|
value: {
|
||||||
|
"sections-enabled": {
|
||||||
|
"feature-ui": 1,
|
||||||
|
"feature-setting-value": 0,
|
||||||
|
"dialog-re-engage-time": 4,
|
||||||
|
"feature-setting-value-pbm": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unified-search:
|
||||||
|
description: A feature allowing user to easily search for specified results directly in the search bar.
|
||||||
|
variables:
|
||||||
|
enabled:
|
||||||
|
description: If true, the feature shows up in the search bar.
|
||||||
|
type: Boolean
|
||||||
|
default: true
|
||||||
|
|
||||||
|
extensions-process:
|
||||||
|
description: A feature to rollout the extensions process.
|
||||||
|
variables:
|
||||||
|
enabled:
|
||||||
|
description: If true, the extensions process is enabled.
|
||||||
|
type: Boolean
|
||||||
|
default: true
|
||||||
|
|
||||||
|
growth-data:
|
||||||
|
description: A feature measuring campaign growth data
|
||||||
|
variables:
|
||||||
|
enabled:
|
||||||
|
description: If true, the feature is active
|
||||||
|
type: Boolean
|
||||||
|
default: false
|
||||||
|
defaults:
|
||||||
|
- channel: release
|
||||||
|
value:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
re-engagement-notification:
|
||||||
|
description: A feature that shows the re-engagement notification if the user is inactive.
|
||||||
|
variables:
|
||||||
|
enabled:
|
||||||
|
description: If true, the re-engagement notification is shown to the inactive user.
|
||||||
|
type: Boolean
|
||||||
|
default: false
|
||||||
|
type:
|
||||||
|
description: The type of re-engagement notification that is shown to the inactive user.
|
||||||
|
type: Int
|
||||||
|
default: 0
|
||||||
|
|
||||||
|
pre-permission-notification-prompt:
|
||||||
|
description: A feature that shows the pre-permission notification prompt.
|
||||||
|
variables:
|
||||||
|
enabled:
|
||||||
|
description: if true, the pre-permission notification prompt is shown to the user.
|
||||||
|
type: Boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
|
onboarding:
|
||||||
|
description: "A feature that configures the new user onboarding page.
|
||||||
|
Note that onboarding is a **first run** feature, and should only be modified by first run experiments."
|
||||||
|
variables:
|
||||||
|
order:
|
||||||
|
description: Determines the order of the onboarding page panels
|
||||||
|
type: List<OnboardingPanel>
|
||||||
|
default: ["themes", "toolbar-placement", "sync", "tcp", "privacy-notice"]
|
||||||
|
|
||||||
|
glean:
|
||||||
|
description: "A feature that provides server-side configurations for Glean metrics (aka Server Knobs)."
|
||||||
|
variables:
|
||||||
|
metrics-enabled:
|
||||||
|
description: "A map of metric base-identifiers to booleans representing the state of the 'enabled' flag for that metric."
|
||||||
|
type: Map<String, Boolean>
|
||||||
|
default: {}
|
||||||
|
enable-event-timestamps:
|
||||||
|
description: "Enables precise event timestamps for Glean events"
|
||||||
|
type: Boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
|
splash-screen:
|
||||||
|
description: "A feature that extends splash screen duration, allowing additional data fetching time for the app's initial run."
|
||||||
|
variables:
|
||||||
|
enabled:
|
||||||
|
description: "If true, the feature is active."
|
||||||
|
type: Boolean
|
||||||
|
default: false
|
||||||
|
maximum_duration_ms:
|
||||||
|
description: The maximum amount of time in milliseconds the splashscreen will be visible while waiting for initialization calls to complete.
|
||||||
|
type: Int
|
||||||
|
default: 0
|
||||||
|
|
||||||
|
shopping-experience:
|
||||||
|
description: A feature that shows product review quality information.
|
||||||
|
variables:
|
||||||
|
enabled:
|
||||||
|
description: if true, the shopping experience feature is shown to the user.
|
||||||
|
type: Boolean
|
||||||
|
default: false
|
||||||
|
product-recommendations:
|
||||||
|
description: if true, recommended products feature is enabled to be shown to the user based on their preference.
|
||||||
|
type: Boolean
|
||||||
|
default: false
|
||||||
|
defaults:
|
||||||
|
- channel: developer
|
||||||
|
value:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
print:
|
||||||
|
description: A feature for printing from the share or browser menu.
|
||||||
|
variables:
|
||||||
|
share-print-enabled:
|
||||||
|
description: If true, a print button from the share menu is available.
|
||||||
|
type: Boolean
|
||||||
|
default: true
|
||||||
|
browser-print-enabled:
|
||||||
|
description: If true, a print button from the browser menu is available.
|
||||||
|
type: Boolean
|
||||||
|
default: true
|
||||||
|
|
||||||
|
search-extra-params:
|
||||||
|
description: A feature that provides additional args for search.
|
||||||
|
variables:
|
||||||
|
enabled:
|
||||||
|
description: If true, the feature is active.
|
||||||
|
type: Boolean
|
||||||
|
default: false
|
||||||
|
search-engine:
|
||||||
|
description: The search engine name.
|
||||||
|
type: String
|
||||||
|
default: ""
|
||||||
|
feature-enabler:
|
||||||
|
description: The feature enabler param name with arg, NOTE this map could be empty.
|
||||||
|
type: Map<String, String>
|
||||||
|
default: {}
|
||||||
|
channel-id:
|
||||||
|
description: The channel Id param name with arg.
|
||||||
|
type: Map<String, String>
|
||||||
|
default: {}
|
||||||
|
|
||||||
|
fx-suggest:
|
||||||
|
description: A feature that provides Firefox Suggest search suggestions.
|
||||||
|
variables:
|
||||||
|
enabled:
|
||||||
|
description: >
|
||||||
|
Whether the feature is enabled. When Firefox Suggest is enabled,
|
||||||
|
Firefox will download and store new search suggestions in the
|
||||||
|
background, and show additional Search settings to control which
|
||||||
|
suggestions appear in the awesomebar. When Firefox Suggest is
|
||||||
|
disabled, Firefox will not download new suggestions, and hide the
|
||||||
|
additional Search settings.
|
||||||
|
type: Boolean
|
||||||
|
default: false
|
||||||
|
defaults:
|
||||||
|
- channel: developer
|
||||||
|
value:
|
||||||
|
enabled: true
|
||||||
|
- channel: nightly
|
||||||
|
value:
|
||||||
|
enabled: true
|
||||||
|
types:
|
||||||
|
objects: {}
|
||||||
|
|
||||||
|
enums:
|
||||||
|
HomeScreenSection:
|
||||||
|
description: The identifiers for the sections of the homescreen.
|
||||||
|
variants:
|
||||||
|
top-sites:
|
||||||
|
description: The frecency and pinned sites.
|
||||||
|
recently-saved:
|
||||||
|
description: The sites the user has bookmarked recently.
|
||||||
|
jump-back-in:
|
||||||
|
description: The tabs the user was looking immediately before being interrupted.
|
||||||
|
recent-explorations:
|
||||||
|
description: The tab groups
|
||||||
|
pocket:
|
||||||
|
description: The pocket section. This should only be available in the US.
|
||||||
|
pocket-sponsored-stories:
|
||||||
|
description: Subsection of the Pocket homescreen section which shows sponsored stories.
|
||||||
|
|
||||||
|
MR2022Section:
|
||||||
|
description: The identifiers for the sections of the MR 2022.
|
||||||
|
variants:
|
||||||
|
home-onboarding-dialog-existing-users:
|
||||||
|
description: Home onboarding dialog for upgraded users.
|
||||||
|
sync-cfr:
|
||||||
|
description: CFR for the first time you see a synced tab on the home screen.
|
||||||
|
wallpapers-selection-tool:
|
||||||
|
description: Wallpapers selection dialog tool for the home screen.
|
||||||
|
jump-back-in-cfr:
|
||||||
|
description: Jump back-in onboarding message.
|
||||||
|
tcp-cfr:
|
||||||
|
description: CFR for the first time you use the browse with Total Cookie Protection on the browser screen.
|
||||||
|
tcp-feature:
|
||||||
|
description: Controls the Total Cookie Protection feature.
|
||||||
|
CookieBannersSection:
|
||||||
|
description: The identifiers for the sections of the MR 2022.
|
||||||
|
variants:
|
||||||
|
feature-ui:
|
||||||
|
description: An integer either 0 or 1 indicating if the UI for cookie banner handling should be visible,
|
||||||
|
0 to hide the UI and 1 to show the UI. The actual UI is composed by cookie banner section
|
||||||
|
in the settings page, the toolbar section and the re-engagement dialog.
|
||||||
|
feature-setting-value:
|
||||||
|
description: An integer either 0 or 1 indicating if cookie banner setting should be enabled or disabled,
|
||||||
|
0 for setting the value to disabled, 1 for enabling the setting with the value reject_all.
|
||||||
|
dialog-re-engage-time:
|
||||||
|
description: An integer indicating the number of hours that needs to happen before
|
||||||
|
the re-engagement dialog shows again since the last seen, for example if set to 4
|
||||||
|
that means if the users has seen the dialog, it will see it 4 hours later.
|
||||||
|
feature-setting-value-pbm:
|
||||||
|
description: An integer either 0 or 1 indicating if cookie banner setting should be enabled or disabled,
|
||||||
|
0 for setting the value to disabled, 1 for enabling the setting with the value reject_all.
|
||||||
|
OnboardingPanel:
|
||||||
|
description: The types of onboarding panels in the onboarding page
|
||||||
|
variants:
|
||||||
|
themes:
|
||||||
|
description: The themes onboarding panel where users pick themes
|
||||||
|
toolbar-placement:
|
||||||
|
description: The onboarding panel where users choose their toolbar placement (bottom or top)
|
||||||
|
sync:
|
||||||
|
description: The onboarding panel where users can sign in to sync
|
||||||
|
tcp:
|
||||||
|
description: The onboarding panel where users can choose their total cookie protection settings
|
||||||
|
privacy-notice:
|
||||||
|
description: The onboarding panel where users can tap to view our privacy notice.
|
|
@ -0,0 +1,114 @@
|
||||||
|
---
|
||||||
|
features:
|
||||||
|
|
||||||
|
juno-onboarding:
|
||||||
|
description: A feature that shows juno onboarding flow.
|
||||||
|
|
||||||
|
variables:
|
||||||
|
cards:
|
||||||
|
description: Collection of user facing onboarding cards.
|
||||||
|
type: Map<String, OnboardingCardData>
|
||||||
|
default:
|
||||||
|
default-browser:
|
||||||
|
card-type: default-browser
|
||||||
|
title: juno_onboarding_default_browser_title_nimbus_2
|
||||||
|
ordering: 10
|
||||||
|
body: juno_onboarding_default_browser_description_nimbus_2
|
||||||
|
link-text: juno_onboarding_default_browser_description_link_text
|
||||||
|
image-res: ic_onboarding_welcome
|
||||||
|
primary-button-label: juno_onboarding_default_browser_positive_button
|
||||||
|
secondary-button-label: juno_onboarding_default_browser_negative_button
|
||||||
|
|
||||||
|
add-search-widget:
|
||||||
|
card-type: add-search-widget
|
||||||
|
enabled: false
|
||||||
|
title: juno_onboarding_add_search_widget_title
|
||||||
|
body: juno_onboarding_add_search_widget_description
|
||||||
|
image-res: ic_onboarding_search_widget
|
||||||
|
ordering: 15
|
||||||
|
primary-button-label: juno_onboarding_add_search_widget_positive_button
|
||||||
|
secondary-button-label: juno_onboarding_add_search_widget_negative_button
|
||||||
|
|
||||||
|
sync-sign-in:
|
||||||
|
card-type: sync-sign-in
|
||||||
|
title: juno_onboarding_sign_in_title_2
|
||||||
|
body: juno_onboarding_sign_in_description_2
|
||||||
|
image-res: ic_onboarding_sync
|
||||||
|
ordering: 20
|
||||||
|
primary-button-label: juno_onboarding_sign_in_positive_button
|
||||||
|
secondary-button-label: juno_onboarding_sign_in_negative_button
|
||||||
|
|
||||||
|
notification-permission:
|
||||||
|
card-type: notification-permission
|
||||||
|
title: juno_onboarding_enable_notifications_title_nimbus_2
|
||||||
|
body: juno_onboarding_enable_notifications_description_nimbus_2
|
||||||
|
image-res: ic_notification_permission
|
||||||
|
ordering: 30
|
||||||
|
primary-button-label: juno_onboarding_enable_notifications_positive_button
|
||||||
|
secondary-button-label: juno_onboarding_enable_notifications_negative_button
|
||||||
|
|
||||||
|
objects:
|
||||||
|
|
||||||
|
OnboardingCardData:
|
||||||
|
description: An object to describe a user facing onboarding card.
|
||||||
|
fields:
|
||||||
|
card-type:
|
||||||
|
type: OnboardingCardType
|
||||||
|
description: The type of the card.
|
||||||
|
# This should never be defaulted.
|
||||||
|
default: default-browser
|
||||||
|
enabled:
|
||||||
|
type: Boolean
|
||||||
|
description: If true, this card is shown to the user.
|
||||||
|
default: true
|
||||||
|
title:
|
||||||
|
type: Text
|
||||||
|
description: The title text displayed to the user.
|
||||||
|
# This should never be defaulted.
|
||||||
|
default: ""
|
||||||
|
body:
|
||||||
|
type: Text
|
||||||
|
description: The message text displayed to the user. May contain linkable text.
|
||||||
|
# This should never be defaulted.
|
||||||
|
default: ""
|
||||||
|
link-text:
|
||||||
|
type: Option<Text>
|
||||||
|
description: >
|
||||||
|
The text to link from the body text. This should match the linkable text from the body text exactly.
|
||||||
|
e.g. body: This is a policy link
|
||||||
|
link-text: policy link
|
||||||
|
default: null
|
||||||
|
image-res:
|
||||||
|
type: Image
|
||||||
|
description: The resource id of the image to be displayed.
|
||||||
|
# This should never be defaulted.
|
||||||
|
default: ic_onboarding_welcome
|
||||||
|
ordering:
|
||||||
|
type: Int
|
||||||
|
description: Used to sequence the cards.
|
||||||
|
# This should never be defaulted.
|
||||||
|
default: 0
|
||||||
|
primary-button-label:
|
||||||
|
type: Text
|
||||||
|
description: The text to display on the primary button.
|
||||||
|
# This should never be defaulted.
|
||||||
|
default: ""
|
||||||
|
secondary-button-label:
|
||||||
|
type: Text
|
||||||
|
description: The text to display on the secondary button.
|
||||||
|
# This should never be defaulted.
|
||||||
|
default: ""
|
||||||
|
|
||||||
|
enums:
|
||||||
|
|
||||||
|
OnboardingCardType:
|
||||||
|
description: An enum to describe a type of card.
|
||||||
|
variants:
|
||||||
|
default-browser:
|
||||||
|
description: Allows user to set Firefox as the default browser.
|
||||||
|
sync-sign-in:
|
||||||
|
description: Allows user to sync with a Firefox account.
|
||||||
|
notification-permission:
|
||||||
|
description: Allows user to enable notification permission.
|
||||||
|
add-search-widget:
|
||||||
|
description: Allows user to add search widget to homescreen.
|
|
@ -0,0 +1,19 @@
|
||||||
|
---
|
||||||
|
features:
|
||||||
|
|
||||||
|
private-browsing:
|
||||||
|
description: Private Browsing Mode
|
||||||
|
|
||||||
|
variables:
|
||||||
|
felt-privacy-enabled:
|
||||||
|
description: if true, enable felt privacy related UI
|
||||||
|
type: Boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
- channel: developer
|
||||||
|
value:
|
||||||
|
felt-privacy-enabled: false
|
||||||
|
- channel: nightly
|
||||||
|
value:
|
||||||
|
felt-privacy-enabled: false
|
|
@ -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/.
|
||||||
|
---
|
||||||
|
$schema: moz://mozilla.org/schemas/glean/pings/2-0-0
|
||||||
|
|
||||||
|
activation:
|
||||||
|
description: |
|
||||||
|
This ping is intended to provide a measure of the activation of mobile
|
||||||
|
products. It's generated when Fenix starts, right after Glean is
|
||||||
|
initialized. It doesn't include the client_id, since it might be reporting
|
||||||
|
an hashed version of the Google Advertising ID.
|
||||||
|
include_client_id: false
|
||||||
|
bugs:
|
||||||
|
- https://bugzilla.mozilla.com/1538011/
|
||||||
|
- https://bugzilla.mozilla.com/1501822/
|
||||||
|
data_reviews:
|
||||||
|
- https://github.com/mozilla-mobile/fenix/pull/1707#issuecomment-486972209
|
||||||
|
notification_emails:
|
||||||
|
- android-probes@mozilla.com
|
||||||
|
|
||||||
|
first-session:
|
||||||
|
description: |
|
||||||
|
**THIS IS NOT A GENERIC FIRST USE PING** This ping is intended to capture
|
||||||
|
Adjust attribution. Use of this ping for other analyses will result in
|
||||||
|
undesirable outcomes.
|
||||||
|
include_client_id: true
|
||||||
|
send_if_empty: true
|
||||||
|
bugs:
|
||||||
|
- https://github.com/mozilla-mobile/fenix/issues/7295
|
||||||
|
data_reviews:
|
||||||
|
- https://github.com/mozilla-mobile/fenix/pull/8074#issuecomment-586512202
|
||||||
|
notification_emails:
|
||||||
|
- android-probes@mozilla.com
|
||||||
|
|
||||||
|
topsites-impression:
|
||||||
|
description: |
|
||||||
|
Recorded when a sponsored top site is rendered and visible on the home
|
||||||
|
screen. Visibility is qualified as when the homepage is brought to the
|
||||||
|
front of the Browser, and sponsored tiles are 100% visible on screen.
|
||||||
|
include_client_id: false
|
||||||
|
bugs:
|
||||||
|
- https://github.com/mozilla-mobile/fenix/issues/23893
|
||||||
|
data_reviews:
|
||||||
|
- https://github.com/mozilla-mobile/fenix/pull/23945
|
||||||
|
notification_emails:
|
||||||
|
- android-probes@mozilla.com
|
||||||
|
|
||||||
|
spoc:
|
||||||
|
description: |
|
||||||
|
Contains data identifying with which Pocket sponsored story the user
|
||||||
|
interacted with and the type of interaction: story impression or click.
|
||||||
|
include_client_id: false
|
||||||
|
reasons:
|
||||||
|
impression: |
|
||||||
|
A sponsored story had more than 50% of it's content visible
|
||||||
|
on the screen.
|
||||||
|
click: |
|
||||||
|
A sponsored story was clicked by the user.
|
||||||
|
bugs:
|
||||||
|
- https://github.com/mozilla-mobile/fenix/issues/27549
|
||||||
|
- https://mozilla-hub.atlassian.net/browse/FNXV2-21791
|
||||||
|
data_reviews:
|
||||||
|
- https://github.com/mozilla-mobile/fenix/pull/27550#issuecomment-1295027631
|
||||||
|
notification_emails:
|
||||||
|
- android-probes@mozilla.com
|
||||||
|
|
||||||
|
cookie-banner-report-site:
|
||||||
|
description: |
|
||||||
|
This ping is needed when the cookie banner reducer doesn't work on
|
||||||
|
a website, and the user wants to report the site.
|
||||||
|
This ping doesn't include a client id.
|
||||||
|
include_client_id: false
|
||||||
|
bugs:
|
||||||
|
- https://bugzilla.mozilla.org/show_bug.cgi?id=1805450
|
||||||
|
data_reviews:
|
||||||
|
- https://github.com/mozilla-mobile/firefox-android/pull/1298#pullrequestreview-1350344223
|
||||||
|
notification_emails:
|
||||||
|
- android-probes@mozilla.com
|
||||||
|
fx-suggest:
|
||||||
|
description: |
|
||||||
|
A ping representing a single event occurring with or to a Firefox Suggestion.
|
||||||
|
Distinguishable by its `ping_type`.
|
||||||
|
Does not contain a `client_id`, preferring a `context_id` instead.
|
||||||
|
include_client_id: false
|
||||||
|
bugs:
|
||||||
|
- https://bugzilla.mozilla.org/show_bug.cgi?id=1857092
|
||||||
|
data_reviews:
|
||||||
|
- https://github.com/mozilla-mobile/firefox-android/pull/3958#issuecomment-1758607927
|
||||||
|
notification_emails:
|
||||||
|
- lina@mozilla.com
|
||||||
|
- ttran@mozilla.com
|
||||||
|
- najiang@mozilla.com
|
|
@ -0,0 +1,57 @@
|
||||||
|
-dontobfuscate
|
||||||
|
|
||||||
|
####################################################################################################
|
||||||
|
# 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 org.mozilla.fenix.**ViewModel { *; }
|
||||||
|
|
||||||
|
# Keep Android Lifecycle methods
|
||||||
|
# https://bugzilla.mozilla.org/show_bug.cgi?id=1596302
|
||||||
|
-keep class androidx.lifecycle.** { *; }
|
|
@ -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>
|
||||||
|
<p>Apartment, suite, etc. <input id="apartment" 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,17 @@
|
||||||
|
<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>
|
||||||
|
<p> Expiry date:
|
||||||
|
<input id="expiryMonthAndYear" inputmode="numerical" placeholder="MM / YYYY" type="text" />
|
||||||
|
</p>
|
||||||
|
<p><input type="submit" id="submit" value="Submit" aria-label="submit"/></p>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,15 @@
|
||||||
|
<html>
|
||||||
|
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
<!-- This asset is using the code behind
|
||||||
|
- https://www.mozilla-anti-tracking.com/test/dfpi/storage_access_api.html
|
||||||
|
- test page.
|
||||||
|
- Source repository: https://github.com/mozilla/anti-tracking-test-pages -->
|
||||||
|
<body>
|
||||||
|
<h2>Cross-site cookies storage access test</h2>
|
||||||
|
<h3>anti-tracker-test.com</h3>
|
||||||
|
<h4>different site, cross-origin iframe</h4>
|
||||||
|
<iframe width=500 height=1000 src="https://mozilla-mobile.github.io/testapp/anti-tracker-test_set_storage_with_sa_api.html"></iframe>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Html_Control_Form</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<p>Misc Link Types</p>
|
||||||
|
<section>
|
||||||
|
<a href="mailto://example@example.com">Email link</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<a href="tel://1234567890">Telephone link</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<a href="vnd.youtube://@Mozilla">Youtube schema link</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<a href="https://m.youtube.com/user/mozilla?cbrd=1">Youtube full link</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<a href="http://play.google.com/store/apps/details?id=org.mozilla.firefox">Playstore link</a>
|
||||||
|
</section>
|
||||||
|
</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,16 @@
|
||||||
|
<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=org.mozilla.fenix">Mozilla Playstore link</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="../resources/pdfForm.pdf">PDF form file</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>
|
|
@ -0,0 +1,83 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Html_Control_Form</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<p>Calendar Form</p>
|
||||||
|
<section>
|
||||||
|
<input type="date" id="calendar">
|
||||||
|
<button onclick="printDate()" id="submitDate"> Submit date </button>
|
||||||
|
<div id="displayDate"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<p>Clock Form</p>
|
||||||
|
<section>
|
||||||
|
<input type="time" id="clock">
|
||||||
|
<button onclick="printTime()" id="submitTime"> Submit time </button>
|
||||||
|
<div id="displayTime"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<p>Color Form</p>
|
||||||
|
<section>
|
||||||
|
<input type="color" id="colorPicker">
|
||||||
|
<button onclick="printColor()" id="submitColor"> Submit color </button>
|
||||||
|
<div id="displayColor"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<p>Drop-down Form</p>
|
||||||
|
<select id="dropDown">
|
||||||
|
<option type="text" text="The Only Ones">The Only Ones</option>
|
||||||
|
<option type="text" text="The National">The National</option>
|
||||||
|
</select>
|
||||||
|
<button onclick="printOption()" id="submitOption"> Submit drop down option </button>
|
||||||
|
<div id="displayOption"></div>
|
||||||
|
|
||||||
|
</br>
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
<div>
|
||||||
|
<label>Choose file to upload</label>
|
||||||
|
</br>
|
||||||
|
<input type="file" id="upload_file"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function printOption() {
|
||||||
|
let dropDown = document.querySelector("#dropDown");
|
||||||
|
let displayOption = document.querySelector("#displayOption");
|
||||||
|
|
||||||
|
displayOption.innerHTML = "Selected option is: " + dropDown.value;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function printDate() {
|
||||||
|
let calendar = document.querySelector("#calendar");
|
||||||
|
let displayDate = document.querySelector("#displayDate");
|
||||||
|
|
||||||
|
displayDate.innerHTML = "Selected date is: " + calendar.value;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function printTime() {
|
||||||
|
let time = document.querySelector("#clock");
|
||||||
|
let displayTime = document.querySelector("#displayTime");
|
||||||
|
|
||||||
|
displayTime.innerHTML = "Selected time is: " + time.value;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function printColor() {
|
||||||
|
let colorPicker = document.querySelector("#colorPicker");
|
||||||
|
let displayColor = document.querySelector("#displayColor");
|
||||||
|
|
||||||
|
displayColor.innerHTML = "Selected color is: " + colorPicker.value;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</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,49 @@
|
||||||
|
<!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>
|
||||||
|
<head>
|
||||||
|
<title>Muted_Video_Test_Page</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p id="testContent">Page content: muted video player</p>
|
||||||
|
<div class="playbackState">
|
||||||
|
<p>Media file not playing</p>
|
||||||
|
</div>
|
||||||
|
<div id="video-container" style="text-align:center">
|
||||||
|
<button onclick="play()">Play</button>
|
||||||
|
<button onclick="pause()">Pause</button>
|
||||||
|
<button onclick="fullscreen()">Full Screen</button>
|
||||||
|
<br><br>
|
||||||
|
<video id="mutedVideo" width="420" autoplay muted controls loop>
|
||||||
|
<source src="../resources/clip.mp4" type="video/mp4">
|
||||||
|
Your browser does not support HTML video.
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const mutedVideo = document.getElementById("mutedVideo");
|
||||||
|
|
||||||
|
function play() {
|
||||||
|
mutedVideo.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
function pause() {
|
||||||
|
mutedVideo.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
function fullscreen() {
|
||||||
|
mutedVideo.requestFullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
mutedVideo.addEventListener('playing', (event) => {
|
||||||
|
document.querySelector('.playbackState').innerHTML="Media file is playing";
|
||||||
|
});
|
||||||
|
|
||||||
|
mutedVideo.addEventListener('pause', (event) => {
|
||||||
|
document.querySelector('.playbackState').innerHTML="Media file is paused";
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</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,81 @@
|
||||||
|
<!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/. -->
|
||||||
|
<!-- This asset is using the code behind
|
||||||
|
- https://www.mozilla-anti-tracking.com/test/trackingprotection/test_pages/tracking_protection.html
|
||||||
|
- test page.
|
||||||
|
- Source repository: https://github.com/mozilla/anti-tracking-test-pages -->
|
||||||
|
<html dir="ltr" xml:lang="en-US" lang="en-US">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf8">
|
||||||
|
<script src="../resources/trackingAPI.js" type="text/javascript"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h3>Level 1 (Basic) List</h3>
|
||||||
|
<p>social-track-digest256:</p>
|
||||||
|
<img
|
||||||
|
src="https://social-track-digest256.dummytracker.org/test_not_blocked.png" alt="social not blocked"
|
||||||
|
onerror="this.onerror=null;this.src='https://not-a-tracker.dummytracker.org/test_blocked.png';this.alt='social blocked'">
|
||||||
|
<br/>
|
||||||
|
<p>ads-track-digest256:</p>
|
||||||
|
<img
|
||||||
|
src="https://ads-track-digest256.dummytracker.org/test_not_blocked.png" alt="ads not blocked"
|
||||||
|
onerror="this.onerror=null;this.src='https://not-a-tracker.dummytracker.org/test_blocked.png';this.alt='ads blocked'">
|
||||||
|
<br/>
|
||||||
|
<p>analytics-track-digest256:</p>
|
||||||
|
<img
|
||||||
|
src="https://analytics-track-digest256.dummytracker.org/test_not_blocked.png" alt="analytics not blocked"
|
||||||
|
onerror="this.onerror=null;this.src='https://not-a-tracker.dummytracker.org/test_blocked.png';this.alt='analytics blocked'">
|
||||||
|
<br/>
|
||||||
|
<p>Fingerprinting:
|
||||||
|
<pre id="result">test not run</pre>
|
||||||
|
<script src="https://base-fingerprinting-track-digest256.dummytracker.org/tracker.js"
|
||||||
|
onerror="this.onerror=null;var result=document.getElementById('result');result.innerHTML='Fingerprinting blocked';"
|
||||||
|
onload="this.onload=null;var result=document.getElementById('result');result.innerHTML='Fingerprinting not blocked';"
|
||||||
|
></script>
|
||||||
|
</p>
|
||||||
|
<br/>
|
||||||
|
<p>Cryptomining:
|
||||||
|
<img
|
||||||
|
src="https://base-cryptomining-track-digest256.dummytracker.org/test_not_blocked.png" alt="Cryptomining not blocked"
|
||||||
|
onerror="this.onerror=null;this.src='https://not-a-tracker.dummytracker.org/test_blocked.png';this.alt='Cryptomining 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,49 @@
|
||||||
|
<!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>
|
||||||
|
<head>
|
||||||
|
<title>Video_Test_Page</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p id="testContent">Page content: video player</p>
|
||||||
|
<div class="playbackState">
|
||||||
|
<p>Media file not playing</p>
|
||||||
|
</div>
|
||||||
|
<div id="video-container" style="text-align:center">
|
||||||
|
<button onclick="play()">Play</button>
|
||||||
|
<button onclick="pause()">Pause</button>
|
||||||
|
<button onclick="fullscreen()">Full Screen</button>
|
||||||
|
<br><br>
|
||||||
|
<video id="video" width="420" autoplay controls loop>
|
||||||
|
<source src="../resources/clip.mp4" type="video/mp4">
|
||||||
|
Your browser does not support HTML video.
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const video = document.getElementById("video");
|
||||||
|
|
||||||
|
function play() {
|
||||||
|
video.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
function pause() {
|
||||||
|
video.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
function fullscreen() {
|
||||||
|
video.requestFullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
video.addEventListener('playing', (event) => {
|
||||||
|
document.querySelector('.playbackState').innerHTML="Media file is playing";
|
||||||
|
});
|
||||||
|
|
||||||
|
video.addEventListener('pause', (event) => {
|
||||||
|
document.querySelector('.playbackState').innerHTML="Media file is paused";
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Binary file not shown.
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,201 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix
|
||||||
|
|
||||||
|
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 org.mozilla.fenix.GleanMetrics.ErrorPage
|
||||||
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.ext.isOnline
|
||||||
|
import org.mozilla.fenix.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
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
|
||||||
|
ErrorPage.visitedError.record(ErrorPage.VisitedErrorExtra(improvedErrorType.name))
|
||||||
|
|
||||||
|
val errorPageUri = ErrorPages.createUrlEncodedErrorPage(
|
||||||
|
context = context,
|
||||||
|
errorType = improvedErrorType,
|
||||||
|
uri = uri,
|
||||||
|
htmlResource = riskLevel.htmlRes,
|
||||||
|
titleOverride = { type -> getErrorPageTitle(context, type) },
|
||||||
|
descriptionOverride = { type -> getErrorPageDescription(context, type) },
|
||||||
|
)
|
||||||
|
|
||||||
|
return RequestInterceptor.ErrorResponse(errorPageUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method is the only difference from the production code.
|
||||||
|
// Otherwise the code should be kept identical
|
||||||
|
@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
|
||||||
|
errorType == ErrorType.ERROR_HTTPS_ONLY -> ErrorType.ERROR_HTTPS_ONLY
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getErrorPageTitle(context: Context, type: ErrorType): String? {
|
||||||
|
return when (type) {
|
||||||
|
ErrorType.ERROR_HTTPS_ONLY -> context.getString(R.string.errorpage_httpsonly_title)
|
||||||
|
// Returning `null` will let the component use its default title for this error type
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getErrorPageDescription(context: Context, type: ErrorType): String? {
|
||||||
|
return when (type) {
|
||||||
|
ErrorType.ERROR_HTTPS_ONLY ->
|
||||||
|
context.getString(R.string.errorpage_httpsonly_message_title) +
|
||||||
|
"<br><br>" +
|
||||||
|
context.getString(R.string.errorpage_httpsonly_message_summary)
|
||||||
|
// Returning `null` will let the component use its default description for this error type
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 org.mozilla.fenix.components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import mozilla.components.service.fxa.ServerConfig
|
||||||
|
import mozilla.components.service.fxa.ServerConfig.Server
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility to configure Firefox 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(Server.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 org.mozilla.fenix.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,74 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.experimentintegration
|
||||||
|
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mozilla.fenix.ext.settings
|
||||||
|
import org.mozilla.fenix.helpers.HomeActivityTestRule
|
||||||
|
import org.mozilla.fenix.helpers.TestHelper
|
||||||
|
import org.mozilla.fenix.ui.robots.homeScreen
|
||||||
|
|
||||||
|
class GenericExperimentIntegrationTest {
|
||||||
|
private val experimentName = "Viewpoint"
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val activityTestRule = HomeActivityTestRule(
|
||||||
|
isJumpBackInCFREnabled = false,
|
||||||
|
isPWAsPromptEnabled = false,
|
||||||
|
isTCPCFREnabled = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
TestHelper.appContext.settings().showSecretDebugMenuThisSession = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
TestHelper.appContext.settings().showSecretDebugMenuThisSession = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun disableStudiesViaStudiesToggle() {
|
||||||
|
homeScreen {
|
||||||
|
}.openThreeDotMenu {
|
||||||
|
}.openSettings {
|
||||||
|
}.openExperimentsMenu {
|
||||||
|
verifyExperimentEnrolled(experimentName)
|
||||||
|
}.goBack {
|
||||||
|
}.openSettingsSubMenuDataCollection {
|
||||||
|
clickStudiesOption()
|
||||||
|
verifyStudiesToggle(true)
|
||||||
|
clickStudiesToggle()
|
||||||
|
clickStudiesDialogOkButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testExperimentUnenrolled() {
|
||||||
|
homeScreen {
|
||||||
|
}.openThreeDotMenu {
|
||||||
|
}.openSettings {
|
||||||
|
}.openExperimentsMenu {
|
||||||
|
verifyExperimentExists(experimentName)
|
||||||
|
verifyExperimentNotEnrolled(experimentName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testExperimentUnenrolledViaSecretMenu() {
|
||||||
|
homeScreen {
|
||||||
|
}.openThreeDotMenu {
|
||||||
|
}.openSettings {
|
||||||
|
}.openExperimentsMenu {
|
||||||
|
verifyExperimentExists(experimentName)
|
||||||
|
verifyExperimentEnrolled(experimentName)
|
||||||
|
unenrollfromExperiment(experimentName)
|
||||||
|
verifyExperimentNotEnrolled(experimentName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
[[source]]
|
||||||
|
url = "https://pypi.python.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
name = "pypi"
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
pytest = "*"
|
||||||
|
pytest-html = "*"
|
||||||
|
pytest-metadata = "*"
|
||||||
|
pytest-variables = "*"
|
||||||
|
pyyaml = "*"
|
||||||
|
requests = "*"
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
black = "*"
|
||||||
|
flake8 = "*"
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
python_version = "3.11"
|
445
app/src/androidTest/java/org/mozilla/fenix/experimentintegration/Pipfile.lock
generated
Normal file
445
app/src/androidTest/java/org/mozilla/fenix/experimentintegration/Pipfile.lock
generated
Normal file
|
@ -0,0 +1,445 @@
|
||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"hash": {
|
||||||
|
"sha256": "53501c7e751ae79697bf8c7289b6095f49fed97242fe186fea42989e800c39d5"
|
||||||
|
},
|
||||||
|
"pipfile-spec": 6,
|
||||||
|
"requires": {
|
||||||
|
"python_version": "3.11"
|
||||||
|
},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "pypi",
|
||||||
|
"url": "https://pypi.python.org/simple",
|
||||||
|
"verify_ssl": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"certifi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082",
|
||||||
|
"sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==2023.7.22"
|
||||||
|
},
|
||||||
|
"charset-normalizer": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:02673e456dc5ab13659f85196c534dc596d4ef260e4d86e856c3b2773ce09843",
|
||||||
|
"sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786",
|
||||||
|
"sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e",
|
||||||
|
"sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8",
|
||||||
|
"sha256:09c77f964f351a7369cc343911e0df63e762e42bac24cd7d18525961c81754f4",
|
||||||
|
"sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa",
|
||||||
|
"sha256:1063da2c85b95f2d1a430f1c33b55c9c17ffaf5e612e10aeaad641c55a9e2b9d",
|
||||||
|
"sha256:12ebea541c44fdc88ccb794a13fe861cc5e35d64ed689513a5c03d05b53b7c82",
|
||||||
|
"sha256:153e7b6e724761741e0974fc4dcd406d35ba70b92bfe3fedcb497226c93b9da7",
|
||||||
|
"sha256:15b26ddf78d57f1d143bdf32e820fd8935d36abe8a25eb9ec0b5a71c82eb3895",
|
||||||
|
"sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d",
|
||||||
|
"sha256:187d18082694a29005ba2944c882344b6748d5be69e3a89bf3cc9d878e548d5a",
|
||||||
|
"sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382",
|
||||||
|
"sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678",
|
||||||
|
"sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b",
|
||||||
|
"sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e",
|
||||||
|
"sha256:249c6470a2b60935bafd1d1d13cd613f8cd8388d53461c67397ee6a0f5dce741",
|
||||||
|
"sha256:24a91a981f185721542a0b7c92e9054b7ab4fea0508a795846bc5b0abf8118d4",
|
||||||
|
"sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596",
|
||||||
|
"sha256:250c9eb0f4600361dd80d46112213dff2286231d92d3e52af1e5a6083d10cad9",
|
||||||
|
"sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69",
|
||||||
|
"sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c",
|
||||||
|
"sha256:2f4a0033ce9a76e391542c182f0d48d084855b5fcba5010f707c8e8c34663d77",
|
||||||
|
"sha256:30a85aed0b864ac88309b7d94be09f6046c834ef60762a8833b660139cfbad13",
|
||||||
|
"sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459",
|
||||||
|
"sha256:3ae38d325b512f63f8da31f826e6cb6c367336f95e418137286ba362925c877e",
|
||||||
|
"sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7",
|
||||||
|
"sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908",
|
||||||
|
"sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a",
|
||||||
|
"sha256:468d2a840567b13a590e67dd276c570f8de00ed767ecc611994c301d0f8c014f",
|
||||||
|
"sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8",
|
||||||
|
"sha256:542da1178c1c6af8873e143910e2269add130a299c9106eef2594e15dae5e482",
|
||||||
|
"sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d",
|
||||||
|
"sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d",
|
||||||
|
"sha256:588245972aca710b5b68802c8cad9edaa98589b1b42ad2b53accd6910dad3545",
|
||||||
|
"sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34",
|
||||||
|
"sha256:619d1c96099be5823db34fe89e2582b336b5b074a7f47f819d6b3a57ff7bdb86",
|
||||||
|
"sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6",
|
||||||
|
"sha256:67b8cc9574bb518ec76dc8e705d4c39ae78bb96237cb533edac149352c1f39fe",
|
||||||
|
"sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e",
|
||||||
|
"sha256:70f1d09c0d7748b73290b29219e854b3207aea922f839437870d8cc2168e31cc",
|
||||||
|
"sha256:750b446b2ffce1739e8578576092179160f6d26bd5e23eb1789c4d64d5af7dc7",
|
||||||
|
"sha256:7966951325782121e67c81299a031f4c115615e68046f79b85856b86ebffc4cd",
|
||||||
|
"sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c",
|
||||||
|
"sha256:7f5d10bae5d78e4551b7be7a9b29643a95aded9d0f602aa2ba584f0388e7a557",
|
||||||
|
"sha256:805dfea4ca10411a5296bcc75638017215a93ffb584c9e344731eef0dcfb026a",
|
||||||
|
"sha256:81bf654678e575403736b85ba3a7867e31c2c30a69bc57fe88e3ace52fb17b89",
|
||||||
|
"sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078",
|
||||||
|
"sha256:85a32721ddde63c9df9ebb0d2045b9691d9750cb139c161c80e500d210f5e26e",
|
||||||
|
"sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4",
|
||||||
|
"sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403",
|
||||||
|
"sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0",
|
||||||
|
"sha256:93aa7eef6ee71c629b51ef873991d6911b906d7312c6e8e99790c0f33c576f89",
|
||||||
|
"sha256:96c2b49eb6a72c0e4991d62406e365d87067ca14c1a729a870d22354e6f68115",
|
||||||
|
"sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9",
|
||||||
|
"sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05",
|
||||||
|
"sha256:a0ac5e7015a5920cfce654c06618ec40c33e12801711da6b4258af59a8eff00a",
|
||||||
|
"sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec",
|
||||||
|
"sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56",
|
||||||
|
"sha256:aae32c93e0f64469f74ccc730a7cb21c7610af3a775157e50bbd38f816536b38",
|
||||||
|
"sha256:aaf7b34c5bc56b38c931a54f7952f1ff0ae77a2e82496583b247f7c969eb1479",
|
||||||
|
"sha256:abecce40dfebbfa6abf8e324e1860092eeca6f7375c8c4e655a8afb61af58f2c",
|
||||||
|
"sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e",
|
||||||
|
"sha256:ac71b2977fb90c35d41c9453116e283fac47bb9096ad917b8819ca8b943abecd",
|
||||||
|
"sha256:ada214c6fa40f8d800e575de6b91a40d0548139e5dc457d2ebb61470abf50186",
|
||||||
|
"sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455",
|
||||||
|
"sha256:b1121de0e9d6e6ca08289583d7491e7fcb18a439305b34a30b20d8215922d43c",
|
||||||
|
"sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65",
|
||||||
|
"sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78",
|
||||||
|
"sha256:b5bcf60a228acae568e9911f410f9d9e0d43197d030ae5799e20dca8df588287",
|
||||||
|
"sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df",
|
||||||
|
"sha256:c2af80fb58f0f24b3f3adcb9148e6203fa67dd3f61c4af146ecad033024dde43",
|
||||||
|
"sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1",
|
||||||
|
"sha256:c5a74c359b2d47d26cdbbc7845e9662d6b08a1e915eb015d044729e92e7050b7",
|
||||||
|
"sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989",
|
||||||
|
"sha256:d47ecf253780c90ee181d4d871cd655a789da937454045b17b5798da9393901a",
|
||||||
|
"sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63",
|
||||||
|
"sha256:d97d85fa63f315a8bdaba2af9a6a686e0eceab77b3089af45133252618e70884",
|
||||||
|
"sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649",
|
||||||
|
"sha256:dc45229747b67ffc441b3de2f3ae5e62877a282ea828a5bdb67883c4ee4a8810",
|
||||||
|
"sha256:e0fc42822278451bc13a2e8626cf2218ba570f27856b536e00cfa53099724828",
|
||||||
|
"sha256:e39c7eb31e3f5b1f88caff88bcff1b7f8334975b46f6ac6e9fc725d829bc35d4",
|
||||||
|
"sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2",
|
||||||
|
"sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd",
|
||||||
|
"sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5",
|
||||||
|
"sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe",
|
||||||
|
"sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293",
|
||||||
|
"sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e",
|
||||||
|
"sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e",
|
||||||
|
"sha256:fc52b79d83a3fe3a360902d3f5d79073a993597d48114c29485e9431092905d8"
|
||||||
|
],
|
||||||
|
"markers": "python_full_version >= '3.7.0'",
|
||||||
|
"version": "==3.3.0"
|
||||||
|
},
|
||||||
|
"idna": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
|
||||||
|
"sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
|
"version": "==3.4"
|
||||||
|
},
|
||||||
|
"iniconfig": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
|
||||||
|
"sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==2.0.0"
|
||||||
|
},
|
||||||
|
"jinja2": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
|
||||||
|
"sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==3.1.2"
|
||||||
|
},
|
||||||
|
"markupsafe": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e",
|
||||||
|
"sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e",
|
||||||
|
"sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431",
|
||||||
|
"sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686",
|
||||||
|
"sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c",
|
||||||
|
"sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559",
|
||||||
|
"sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc",
|
||||||
|
"sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb",
|
||||||
|
"sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939",
|
||||||
|
"sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c",
|
||||||
|
"sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0",
|
||||||
|
"sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4",
|
||||||
|
"sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9",
|
||||||
|
"sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575",
|
||||||
|
"sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba",
|
||||||
|
"sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d",
|
||||||
|
"sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd",
|
||||||
|
"sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3",
|
||||||
|
"sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00",
|
||||||
|
"sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155",
|
||||||
|
"sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac",
|
||||||
|
"sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52",
|
||||||
|
"sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f",
|
||||||
|
"sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8",
|
||||||
|
"sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b",
|
||||||
|
"sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007",
|
||||||
|
"sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24",
|
||||||
|
"sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea",
|
||||||
|
"sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198",
|
||||||
|
"sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0",
|
||||||
|
"sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee",
|
||||||
|
"sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be",
|
||||||
|
"sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2",
|
||||||
|
"sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1",
|
||||||
|
"sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707",
|
||||||
|
"sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6",
|
||||||
|
"sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c",
|
||||||
|
"sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58",
|
||||||
|
"sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823",
|
||||||
|
"sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779",
|
||||||
|
"sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636",
|
||||||
|
"sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c",
|
||||||
|
"sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad",
|
||||||
|
"sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee",
|
||||||
|
"sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc",
|
||||||
|
"sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2",
|
||||||
|
"sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48",
|
||||||
|
"sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7",
|
||||||
|
"sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e",
|
||||||
|
"sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b",
|
||||||
|
"sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa",
|
||||||
|
"sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5",
|
||||||
|
"sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e",
|
||||||
|
"sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb",
|
||||||
|
"sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9",
|
||||||
|
"sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57",
|
||||||
|
"sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc",
|
||||||
|
"sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc",
|
||||||
|
"sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2",
|
||||||
|
"sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==2.1.3"
|
||||||
|
},
|
||||||
|
"packaging": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5",
|
||||||
|
"sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==23.2"
|
||||||
|
},
|
||||||
|
"pluggy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12",
|
||||||
|
"sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==1.3.0"
|
||||||
|
},
|
||||||
|
"pytest": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2f2301e797521b23e4d2585a0a3d7b5e50fdddaaf7e7d6773ea26ddb17c213ab",
|
||||||
|
"sha256:460c9a59b14e27c602eb5ece2e47bec99dc5fc5f6513cf924a7d03a578991b1f"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==7.4.1"
|
||||||
|
},
|
||||||
|
"pytest-html": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3b473cc278272f8b5a34cd3bf10f88ac5fcb17cb5af22f9323514af00c310e64",
|
||||||
|
"sha256:79c4677ed6196417bf290d8b81f706342ae49f726f623728efa3f7dfff09f8eb"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==4.0.0"
|
||||||
|
},
|
||||||
|
"pytest-metadata": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:769a9c65d2884bd583bc626b0ace77ad15dbe02dd91a9106d47fd46d9c2569ca",
|
||||||
|
"sha256:a17b1e40080401dc23177599208c52228df463db191c1a573ccdffacd885e190"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==3.0.0"
|
||||||
|
},
|
||||||
|
"pytest-variables": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:190d9d4da5a6013eb02df2049f6047d911cdbe44c5b1734a6acc1748433c93d0",
|
||||||
|
"sha256:ab84235417afac5a0a7dd4c3918287d9c7329d2e16d570d6e943f8d8e02533b9"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==3.0.0"
|
||||||
|
},
|
||||||
|
"pyyaml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5",
|
||||||
|
"sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc",
|
||||||
|
"sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df",
|
||||||
|
"sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741",
|
||||||
|
"sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206",
|
||||||
|
"sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27",
|
||||||
|
"sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595",
|
||||||
|
"sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62",
|
||||||
|
"sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98",
|
||||||
|
"sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696",
|
||||||
|
"sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290",
|
||||||
|
"sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9",
|
||||||
|
"sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d",
|
||||||
|
"sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6",
|
||||||
|
"sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867",
|
||||||
|
"sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47",
|
||||||
|
"sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486",
|
||||||
|
"sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6",
|
||||||
|
"sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3",
|
||||||
|
"sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007",
|
||||||
|
"sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938",
|
||||||
|
"sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0",
|
||||||
|
"sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c",
|
||||||
|
"sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735",
|
||||||
|
"sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d",
|
||||||
|
"sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28",
|
||||||
|
"sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4",
|
||||||
|
"sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba",
|
||||||
|
"sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8",
|
||||||
|
"sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5",
|
||||||
|
"sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd",
|
||||||
|
"sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3",
|
||||||
|
"sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0",
|
||||||
|
"sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515",
|
||||||
|
"sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c",
|
||||||
|
"sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c",
|
||||||
|
"sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924",
|
||||||
|
"sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34",
|
||||||
|
"sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43",
|
||||||
|
"sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859",
|
||||||
|
"sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673",
|
||||||
|
"sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54",
|
||||||
|
"sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a",
|
||||||
|
"sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b",
|
||||||
|
"sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab",
|
||||||
|
"sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa",
|
||||||
|
"sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c",
|
||||||
|
"sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585",
|
||||||
|
"sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d",
|
||||||
|
"sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==6.0.1"
|
||||||
|
},
|
||||||
|
"requests": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
|
||||||
|
"sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==2.31.0"
|
||||||
|
},
|
||||||
|
"urllib3": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84",
|
||||||
|
"sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==2.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"develop": {
|
||||||
|
"black": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3",
|
||||||
|
"sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb",
|
||||||
|
"sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087",
|
||||||
|
"sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320",
|
||||||
|
"sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6",
|
||||||
|
"sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3",
|
||||||
|
"sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc",
|
||||||
|
"sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f",
|
||||||
|
"sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587",
|
||||||
|
"sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91",
|
||||||
|
"sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a",
|
||||||
|
"sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad",
|
||||||
|
"sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926",
|
||||||
|
"sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9",
|
||||||
|
"sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be",
|
||||||
|
"sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd",
|
||||||
|
"sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96",
|
||||||
|
"sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491",
|
||||||
|
"sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2",
|
||||||
|
"sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a",
|
||||||
|
"sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f",
|
||||||
|
"sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==23.7.0"
|
||||||
|
},
|
||||||
|
"click": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28",
|
||||||
|
"sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==8.1.7"
|
||||||
|
},
|
||||||
|
"flake8": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23",
|
||||||
|
"sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_full_version >= '3.8.1'",
|
||||||
|
"version": "==6.1.0"
|
||||||
|
},
|
||||||
|
"mccabe": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
|
||||||
|
"sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==0.7.0"
|
||||||
|
},
|
||||||
|
"mypy-extensions": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d",
|
||||||
|
"sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
|
"version": "==1.0.0"
|
||||||
|
},
|
||||||
|
"packaging": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5",
|
||||||
|
"sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==23.2"
|
||||||
|
},
|
||||||
|
"pathspec": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20",
|
||||||
|
"sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==0.11.2"
|
||||||
|
},
|
||||||
|
"platformdirs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3",
|
||||||
|
"sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==3.11.0"
|
||||||
|
},
|
||||||
|
"pycodestyle": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f",
|
||||||
|
"sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==2.11.1"
|
||||||
|
},
|
||||||
|
"pyflakes": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774",
|
||||||
|
"sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==3.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.experimentintegration
|
||||||
|
|
||||||
|
import android.content.pm.ActivityInfo
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mozilla.fenix.ext.settings
|
||||||
|
import org.mozilla.fenix.helpers.HomeActivityTestRule
|
||||||
|
import org.mozilla.fenix.helpers.TestHelper
|
||||||
|
import org.mozilla.fenix.ui.robots.browserScreen
|
||||||
|
import org.mozilla.fenix.ui.robots.homeScreen
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for verifying functionality of the message survey surface
|
||||||
|
*/
|
||||||
|
class SurveyExperimentIntegrationTest {
|
||||||
|
private val surveyURL = "qsurvey.mozilla.com"
|
||||||
|
private val experimentName = "Viewpoint"
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val activityTestRule = HomeActivityTestRule(
|
||||||
|
isJumpBackInCFREnabled = false,
|
||||||
|
isPWAsPromptEnabled = false,
|
||||||
|
isTCPCFREnabled = false,
|
||||||
|
isDeleteSitePermissionsEnabled = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
TestHelper.appContext.settings().showSecretDebugMenuThisSession = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
TestHelper.appContext.settings().showSecretDebugMenuThisSession = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkExperimentExists() {
|
||||||
|
homeScreen {
|
||||||
|
}.openThreeDotMenu {
|
||||||
|
}.openSettings {
|
||||||
|
}.openExperimentsMenu {
|
||||||
|
verifyExperimentExists(experimentName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkSurveyNavigatesCorrectly() {
|
||||||
|
browserScreen {
|
||||||
|
verifySurveyButton()
|
||||||
|
}.clickSurveyButton {
|
||||||
|
verifyUrl(surveyURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkExperimentExists()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkSurveyNoThanksNavigatesCorrectly() {
|
||||||
|
browserScreen {
|
||||||
|
verifySurveyNoThanksButton()
|
||||||
|
}.clickNoThanksSurveyButton {
|
||||||
|
verifyTabCounter("0")
|
||||||
|
}
|
||||||
|
|
||||||
|
checkExperimentExists()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkHomescreenSurveyDismissesCorrectly() {
|
||||||
|
browserScreen {
|
||||||
|
verifyHomeScreenSurveyCloseButton()
|
||||||
|
}.clickHomeScreenSurveyCloseButton {
|
||||||
|
verifyTabCounter("0")
|
||||||
|
verifySurveyButtonDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
checkExperimentExists()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkSurveyLandscapeLooksCorrect() {
|
||||||
|
activityTestRule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||||
|
browserScreen {
|
||||||
|
verifySurveyNoThanksButton()
|
||||||
|
verifySurveyButton()
|
||||||
|
}
|
||||||
|
|
||||||
|
checkExperimentExists()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,178 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from experimentintegration.gradlewbuild import GradlewBuild
|
||||||
|
|
||||||
|
KLAATU_SERVER_URL = "http://localhost:1378"
|
||||||
|
KLAATU_LOCAL_SERVER_URL = "http://localhost:1378"
|
||||||
|
|
||||||
|
here = Path()
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_addoption(parser):
|
||||||
|
parser.addoption(
|
||||||
|
"--experiment", action="store", help="The experiments experimenter URL"
|
||||||
|
)
|
||||||
|
parser.addoption(
|
||||||
|
"--stage", action="store_true", default=None, help="Use the stage server"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="load_branches")
|
||||||
|
def fixture_load_branches(experiment_url):
|
||||||
|
branches = []
|
||||||
|
|
||||||
|
if experiment_url:
|
||||||
|
data = experiment_url
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
data = requests.get(f"{KLAATU_SERVER_URL}/experiment").json()
|
||||||
|
except ConnectionRefusedError:
|
||||||
|
logging.warn("No URL or experiment slug provided, exiting.")
|
||||||
|
exit()
|
||||||
|
else:
|
||||||
|
for item in reversed(data):
|
||||||
|
data = item
|
||||||
|
break
|
||||||
|
experiment = requests.get(data).json()
|
||||||
|
for item in experiment["branches"]:
|
||||||
|
branches.append(item["slug"])
|
||||||
|
return branches
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def gradlewbuild_log(pytestconfig, tmpdir):
|
||||||
|
gradlewbuild_log = f"{tmpdir.join('gradlewbuild.log')}"
|
||||||
|
pytestconfig._gradlewbuild_log = gradlewbuild_log
|
||||||
|
yield gradlewbuild_log
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def gradlewbuild(gradlewbuild_log):
|
||||||
|
yield GradlewBuild(gradlewbuild_log)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="experiment_data")
|
||||||
|
def fixture_experiment_data(experiment_url):
|
||||||
|
data = requests.get(experiment_url).json()
|
||||||
|
for item in data["branches"][0]["features"][0]["value"]["messages"].values():
|
||||||
|
item["surface"] = "homescreen"
|
||||||
|
item["style"] = "URGENT"
|
||||||
|
for count, trigger in enumerate(item["trigger"]):
|
||||||
|
if "USER_EN_SPEAKER" not in trigger:
|
||||||
|
del(item["trigger"][count])
|
||||||
|
return [data]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="experiment_url", scope="module")
|
||||||
|
def fixture_experiment_url(request, variables):
|
||||||
|
url = None
|
||||||
|
|
||||||
|
if slug := request.config.getoption("--experiment"):
|
||||||
|
# Build URL from slug
|
||||||
|
if request.config.getoption("--stage"):
|
||||||
|
url = f"{variables['urls']['stage_server']}/api/v6/experiments/{slug}"
|
||||||
|
else:
|
||||||
|
url = f"{variables['urls']['prod_server']}/api/v6/experiments/{slug}"
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
data = requests.get(f"{KLAATU_SERVER_URL}/experiment").json()
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
logging.error("No URL or experiment slug provided, exiting.")
|
||||||
|
exit()
|
||||||
|
else:
|
||||||
|
for item in data:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
url = item
|
||||||
|
yield url
|
||||||
|
return_data = {"url": url}
|
||||||
|
try:
|
||||||
|
requests.put(f"{KLAATU_SERVER_URL}/experiment", json=return_data)
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="json_data")
|
||||||
|
def fixture_json_data(tmp_path, experiment_data):
|
||||||
|
path = tmp_path / "data"
|
||||||
|
path.mkdir()
|
||||||
|
json_path = path / "data.json"
|
||||||
|
with open(json_path, "w", encoding="utf-8") as f:
|
||||||
|
# URL of experiment/klaatu server
|
||||||
|
data = {"data": experiment_data}
|
||||||
|
json.dump(data, f)
|
||||||
|
return json_path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="experiment_slug")
|
||||||
|
def fixture_experiment_slug(experiment_data):
|
||||||
|
return experiment_data[0]["slug"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="start_app")
|
||||||
|
def fixture_start_app():
|
||||||
|
def _():
|
||||||
|
command = f"nimbus-cli --app fenix --channel developer open"
|
||||||
|
try:
|
||||||
|
out = subprocess.check_output(
|
||||||
|
command,
|
||||||
|
cwd=os.path.join(here, os.pardir),
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
universal_newlines=True,
|
||||||
|
shell=True,
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
out = e.output
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
with open(gradlewbuild_log, "w") as f:
|
||||||
|
f.write(out)
|
||||||
|
time.sleep(
|
||||||
|
15
|
||||||
|
) # Wait a while as there's no real way to know when the app has started
|
||||||
|
|
||||||
|
return _
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="send_test_results", autouse=True)
|
||||||
|
def fixture_send_test_results():
|
||||||
|
yield
|
||||||
|
here = Path()
|
||||||
|
|
||||||
|
with open(f"{here.resolve()}/results/index.html", "rb") as f:
|
||||||
|
files = {"file": f}
|
||||||
|
try:
|
||||||
|
requests.post(f"{KLAATU_SERVER_URL}/test_results", files=files)
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="setup_experiment")
|
||||||
|
def fixture_setup_experiment(experiment_slug, json_data, gradlewbuild_log):
|
||||||
|
def _(branch):
|
||||||
|
logging.info(f"Testing experiment {experiment_slug}, BRANCH: {branch[0]}")
|
||||||
|
command = f"nimbus-cli --app fenix --channel developer enroll {experiment_slug} --branch {branch[0]} --file {json_data} --reset-app"
|
||||||
|
logging.info(f"Running command {command}")
|
||||||
|
try:
|
||||||
|
out = subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
out = e.output
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
with open(gradlewbuild_log, "w") as f:
|
||||||
|
f.write(f"{out}")
|
||||||
|
time.sleep(
|
||||||
|
15
|
||||||
|
) # Wait a while as there's no real way to know when the app has started
|
||||||
|
|
||||||
|
return _
|
|
@ -0,0 +1,45 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from syncintegration.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 = f"adb shell am instrument -w -e class org.mozilla.fenix.experimentintegration.{identifier} org.mozilla.fenix.debug.test/androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
self.logger.info("Running cmd: {}".format(cmd))
|
||||||
|
|
||||||
|
out = ""
|
||||||
|
try:
|
||||||
|
out = subprocess.check_output(
|
||||||
|
cmd, encoding="utf8", shell=True, stderr=subprocess.STDOUT
|
||||||
|
)
|
||||||
|
if "FAILURES" in out:
|
||||||
|
raise (AssertionError(out))
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
out = e.output
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
# Set the path correctly
|
||||||
|
tests_path = (
|
||||||
|
"app/src/androidTest/java/org/mozilla/fenix/experimentintegration/"
|
||||||
|
)
|
||||||
|
os.chdir(tests_path)
|
||||||
|
with open(self.log, "w") as f:
|
||||||
|
f.write(str(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 --self-contained-html --variables=variables.yaml
|
||||||
|
log_cli = true
|
||||||
|
log_cli_level = info
|
|
@ -0,0 +1,12 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("load_branches", [("branch")], indirect=True)
|
||||||
|
def test_experiment_unenrolls_via_studies_toggle(setup_experiment, gradlewbuild, load_branches):
|
||||||
|
setup_experiment(load_branches)
|
||||||
|
gradlewbuild.test("GenericExperimentIntegrationTest#disableStudiesViaStudiesToggle")
|
||||||
|
gradlewbuild.test("GenericExperimentIntegrationTest#testExperimentUnenrolls")
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("load_branches", [("branch")], indirect=True)
|
||||||
|
def test_experiment_unenrolls_via_secret_menu(setup_experiment, gradlewbuild, load_branches):
|
||||||
|
setup_experiment(load_branches)
|
||||||
|
gradlewbuild.test("GenericExperimentIntegrationTest#testExperimentUnenrollsViaSecretMenu")
|
|
@ -0,0 +1,22 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("load_branches", [("branch")], indirect=True)
|
||||||
|
def test_survey_navigates_correctly(setup_experiment, gradlewbuild, load_branches):
|
||||||
|
setup_experiment(load_branches)
|
||||||
|
gradlewbuild.test("SurveyExperimentIntegrationTest#checkSurveyNavigatesCorrectly")
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("load_branches", [("branch")], indirect=True)
|
||||||
|
def test_survey_no_thanks_navigates_correctly(setup_experiment, gradlewbuild, load_branches):
|
||||||
|
setup_experiment(load_branches)
|
||||||
|
gradlewbuild.test("SurveyExperimentIntegrationTest#checkSurveyNoThanksNavigatesCorrectly")
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("load_branches", [("branch")], indirect=True)
|
||||||
|
def test_homescreen_survey_dismisses_correctly(setup_experiment, gradlewbuild, load_branches):
|
||||||
|
setup_experiment(load_branches)
|
||||||
|
gradlewbuild.test("SurveyExperimentIntegrationTest#checkHomescreenSurveyDismissesCorrectly")
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("load_branches", [("branch")], indirect=True)
|
||||||
|
def test_survey_landscape_looks_correct(setup_experiment, gradlewbuild, load_branches):
|
||||||
|
setup_experiment(load_branches)
|
||||||
|
gradlewbuild.test("SurveyExperimentIntegrationTest#checkSurveyLandscapeLooksCorrect")
|
|
@ -0,0 +1,3 @@
|
||||||
|
urls:
|
||||||
|
stage_server: "https://stage.experimenter.nonprod.dataops.mozgcp.net"
|
||||||
|
prod_server: "https://experimenter.services.mozilla.com"
|
|
@ -0,0 +1,75 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.extensions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import mozilla.components.concept.engine.EngineSession
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mozilla.experiments.nimbus.HardcodedNimbusFeatures
|
||||||
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.gecko.GeckoProvider
|
||||||
|
import org.mozilla.fenix.helpers.TestHelper
|
||||||
|
import org.mozilla.fenix.nimbus.FxNimbus
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumentation test for verifying that the extensions process can be controlled with Nimbus.
|
||||||
|
*/
|
||||||
|
class ExtensionProcessTest {
|
||||||
|
private lateinit var context: Context
|
||||||
|
private lateinit var policy: EngineSession.TrackingProtectionPolicy
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
context = TestHelper.appContext
|
||||||
|
policy =
|
||||||
|
context.components.core.trackingProtectionPolicyFactory.createTrackingProtectionPolicy()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_extension_process_can_be_enabled_by_nimbus() {
|
||||||
|
val hardcodedNimbus = HardcodedNimbusFeatures(
|
||||||
|
context,
|
||||||
|
"extensions-process" to JSONObject(
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
""".trimIndent(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
hardcodedNimbus.connectWith(FxNimbus)
|
||||||
|
|
||||||
|
val runtime = GeckoProvider.createRuntimeSettings(context, policy)
|
||||||
|
|
||||||
|
assertTrue(FxNimbus.features.extensionsProcess.value().enabled)
|
||||||
|
assertTrue(runtime.extensionsProcessEnabled!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_extension_process_can_be_disabled_by_nimbus() {
|
||||||
|
val hardcodedNimbus = HardcodedNimbusFeatures(
|
||||||
|
context,
|
||||||
|
"extensions-process" to JSONObject(
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
""".trimIndent(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
hardcodedNimbus.connectWith(FxNimbus)
|
||||||
|
|
||||||
|
val runtime = GeckoProvider.createRuntimeSettings(context, policy)
|
||||||
|
|
||||||
|
assertFalse(FxNimbus.features.extensionsProcess.value().enabled)
|
||||||
|
assertFalse(runtime.extensionsProcessEnabled!!)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,189 @@
|
||||||
|
/* 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 org.mozilla.fenix.glean
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.rule.ActivityTestRule
|
||||||
|
import androidx.test.uiautomator.UiDevice
|
||||||
|
import androidx.test.uiautomator.UiSelector
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
|
||||||
|
import mozilla.components.service.glean.Glean
|
||||||
|
import mozilla.components.service.glean.config.Configuration
|
||||||
|
import mozilla.components.service.glean.net.ConceptFetchHttpUploader
|
||||||
|
import mozilla.components.service.glean.testing.GleanTestLocalServer
|
||||||
|
import okhttp3.mockwebserver.RecordedRequest
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.BeforeClass
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mozilla.fenix.GleanMetrics.GleanBuildInfo
|
||||||
|
import org.mozilla.fenix.HomeActivity
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.helpers.HomeActivityTestRule
|
||||||
|
import org.mozilla.fenix.helpers.MockWebServerHelper
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.zip.GZIPInputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decompress the GZIP returned by the glean-core layer.
|
||||||
|
*
|
||||||
|
* @param data the gzipped [ByteArray] to decompress
|
||||||
|
* @return a [String] containing the uncompressed data.
|
||||||
|
*/
|
||||||
|
fun decompressGZIP(data: ByteArray): String {
|
||||||
|
return GZIPInputStream(ByteArrayInputStream(data)).bufferedReader().use(BufferedReader::readText)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method to get the body of a request as a String.
|
||||||
|
* The UTF8 representation of the request body will be returned.
|
||||||
|
* If the request body is gzipped, it will be decompressed first.
|
||||||
|
*
|
||||||
|
* @return a [String] containing the body of the request.
|
||||||
|
*/
|
||||||
|
fun RecordedRequest.getPlainBody(): String {
|
||||||
|
return if (this.getHeader("Content-Encoding") == "gzip") {
|
||||||
|
val bodyInBytes = this.body.readByteArray()
|
||||||
|
decompressGZIP(bodyInBytes)
|
||||||
|
} else {
|
||||||
|
this.body.readUtf8()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class BaselinePingTest {
|
||||||
|
private val server = MockWebServerHelper.createAlwaysOkMockWebServer()
|
||||||
|
private lateinit var mDevice: UiDevice
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val activityRule: ActivityTestRule<HomeActivity> = HomeActivityTestRule()
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val gleanRule = GleanTestLocalServer(ApplicationProvider.getApplicationContext(), server.port)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@BeforeClass
|
||||||
|
@JvmStatic
|
||||||
|
@OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
|
||||||
|
fun setupOnce() {
|
||||||
|
val httpClient = ConceptFetchHttpUploader(
|
||||||
|
lazy {
|
||||||
|
GeckoViewFetchClient(ApplicationProvider.getApplicationContext())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fenix does not initialize the Glean SDK in tests/debug builds, but this test
|
||||||
|
// requires Glean to be initialized so we need to do it manually. Additionally,
|
||||||
|
// we need to do this on the main thread, as the Glean SDK requires it.
|
||||||
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
|
Glean.initialize(
|
||||||
|
applicationContext = ApplicationProvider.getApplicationContext(),
|
||||||
|
uploadEnabled = true,
|
||||||
|
configuration = Configuration(httpClient = httpClient),
|
||||||
|
buildInfo = GleanBuildInfo.buildInfo,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for a specific ping to be received by the local server and
|
||||||
|
* return its parsed JSON content.
|
||||||
|
*
|
||||||
|
* @param pingName the name of the ping to wait for
|
||||||
|
* @param pingReason the value of the `reason` field for the received ping
|
||||||
|
* @param maxAttempts how many times should a wait be attempted
|
||||||
|
*/
|
||||||
|
private fun waitForPingContent(
|
||||||
|
pingName: String,
|
||||||
|
pingReason: String?,
|
||||||
|
maxAttempts: Int = 3,
|
||||||
|
): JSONObject? {
|
||||||
|
var attempts = 0
|
||||||
|
do {
|
||||||
|
attempts += 1
|
||||||
|
val request = server.takeRequest(20L, TimeUnit.SECONDS) ?: break
|
||||||
|
val docType = request.path!!.split("/")[3]
|
||||||
|
if (pingName == docType) {
|
||||||
|
val parsedPayload = JSONObject(request.getPlainBody())
|
||||||
|
if (pingReason == null) {
|
||||||
|
return parsedPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we requested a specific ping reason, look for it.
|
||||||
|
val reason = parsedPayload.getJSONObject("ping_info").getString("reason")
|
||||||
|
if (reason == pingReason) {
|
||||||
|
return parsedPayload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (attempts < maxAttempts)
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1807288")
|
||||||
|
@Test
|
||||||
|
fun validateBaselinePing() {
|
||||||
|
// Wait for the app to be idle/ready.
|
||||||
|
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
|
||||||
|
mDevice.waitForIdle()
|
||||||
|
|
||||||
|
// Wait for 1 second: this should guarantee we have some valid duration in the
|
||||||
|
// ping.
|
||||||
|
Thread.sleep(1000)
|
||||||
|
|
||||||
|
// Move it to background.
|
||||||
|
mDevice.pressHome()
|
||||||
|
|
||||||
|
// Due to bug 1632184, we need move the activity to foreground again, in order
|
||||||
|
// for a 'background' ping with reason 'foreground' to be generated and also trigger
|
||||||
|
// sending the ping that was submitted on background. This can go away once bug 1634375
|
||||||
|
// is fixed.
|
||||||
|
mDevice.pressRecentApps()
|
||||||
|
mDevice.findObject(
|
||||||
|
UiSelector().descriptionContains(
|
||||||
|
ApplicationProvider.getApplicationContext<Context>().getString(R.string.app_name),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.click()
|
||||||
|
|
||||||
|
// Validate the received data.
|
||||||
|
val baselinePing = waitForPingContent("baseline", "inactive")!!
|
||||||
|
|
||||||
|
val metrics = baselinePing.getJSONObject("metrics")
|
||||||
|
|
||||||
|
// Make sure we have a 'duration' field with a reasonable value: it should be >= 1, since
|
||||||
|
// we slept for 1000ms.
|
||||||
|
val timespans = metrics.getJSONObject("timespan")
|
||||||
|
assertTrue(timespans.getJSONObject("glean.baseline.duration").getLong("value") >= 1L)
|
||||||
|
|
||||||
|
// Make sure there's no errors.
|
||||||
|
val errors = metrics.optJSONObject("labeled_counter")?.keys()
|
||||||
|
errors?.forEach {
|
||||||
|
assertFalse(it.startsWith("glean.error."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 org.mozilla.fenix.helpers
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
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,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 org.mozilla.fenix.helpers
|
||||||
|
|
||||||
|
import org.mozilla.fenix.helpers.TestHelper.getSponsoredShortcutTitle
|
||||||
|
|
||||||
|
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 GOOGLE_DOCS = "com.google.android.apps.docs"
|
||||||
|
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 POCKET_RECOMMENDED_STORIES_UTM_PARAM = "utm_source=pocket-newtab-android"
|
||||||
|
const val LONG_CLICK_DURATION: Long = 5000
|
||||||
|
const val LISTS_MAXSWIPES: Int = 3
|
||||||
|
const val RETRY_COUNT = 3
|
||||||
|
|
||||||
|
val searchEngineCodes = mapOf(
|
||||||
|
"Google" to "client=firefox-b-m",
|
||||||
|
"Bing" to "firefox&pc=MOZB&form=MOZMBA",
|
||||||
|
"DuckDuckGo" to "t=fpas",
|
||||||
|
)
|
||||||
|
|
||||||
|
val firstSponsoredShortcutTitle by lazy { getSponsoredShortcutTitle(2) }
|
||||||
|
val secondSponsoredShortcutTitle by lazy { getSponsoredShortcutTitle(3) }
|
||||||
|
|
||||||
|
// Expected for en-us defaults
|
||||||
|
val defaultTopSitesList by lazy {
|
||||||
|
mapOf(
|
||||||
|
"Google" to "Google",
|
||||||
|
"First sponsored shortcut" to firstSponsoredShortcutTitle,
|
||||||
|
"Second sponsored shortcut" to secondSponsoredShortcutTitle,
|
||||||
|
"Top Articles" to "Top Articles",
|
||||||
|
"Wikipedia" to "Wikipedia",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.helpers
|
||||||
|
|
||||||
|
import org.mozilla.experiments.nimbus.GleanPlumbMessageHelper
|
||||||
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.helpers.TestHelper.appContext
|
||||||
|
|
||||||
|
object Experimentation {
|
||||||
|
val experiments =
|
||||||
|
appContext.components.analytics.experiments
|
||||||
|
|
||||||
|
fun withHelper(block: GleanPlumbMessageHelper.() -> Unit) {
|
||||||
|
val helper = experiments.createMessageHelper()
|
||||||
|
block(helper)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.helpers
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import org.mozilla.fenix.ext.settings
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for querying the status and modifying various features and settings in the application.
|
||||||
|
*/
|
||||||
|
interface FeatureSettingsHelper {
|
||||||
|
/**
|
||||||
|
* Whether the onboarding for existing users should be shown or not.
|
||||||
|
* It should appear only once on the first visit to homescreen.
|
||||||
|
*/
|
||||||
|
var isHomeOnboardingDialogEnabled: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the Pocket stories feature is enabled or not.
|
||||||
|
*/
|
||||||
|
var isPocketEnabled: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the "Jump back in" CFR should be shown or not.
|
||||||
|
* It should appear on the first visit to homescreen given that there is a tab opened.
|
||||||
|
*/
|
||||||
|
var isJumpBackInCFREnabled: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the onboarding dialog for choosing wallpapers should be shown or not.
|
||||||
|
*/
|
||||||
|
var isWallpaperOnboardingEnabled: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the "Jump back in" homescreen section is enabled or not.
|
||||||
|
* It shows the last visited tab on this device and on other synced devices.
|
||||||
|
*/
|
||||||
|
var isRecentTabsFeatureEnabled: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the "Recently visited" homescreen section is enabled or not.
|
||||||
|
* It can show up to 9 history highlights and history groups.
|
||||||
|
*/
|
||||||
|
var isRecentlyVisitedFeatureEnabled: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the onboarding dialog for PWAs should be shown or not.
|
||||||
|
* It can show the first time a website that can be installed as a PWA is accessed.
|
||||||
|
*/
|
||||||
|
var isPWAsPromptEnabled: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the "Site permissions" option is checked in the "Delete browsing data" screen or not.
|
||||||
|
*/
|
||||||
|
var isDeleteSitePermissionsEnabled: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable showing the TCP CFR when accessing a webpage for the first time.
|
||||||
|
*/
|
||||||
|
var isTCPCFREnabled: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current "Enhanced Tracking Protection" policy.
|
||||||
|
* @see ETPPolicy
|
||||||
|
*/
|
||||||
|
var etpPolicy: ETPPolicy
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable cookie banner reduction dialog.
|
||||||
|
*/
|
||||||
|
var isCookieBannerReductionDialogEnabled: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable open in app banner.
|
||||||
|
*/
|
||||||
|
var isOpenInAppBannerEnabled: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable the Tabs Tray to Compose rewrite.
|
||||||
|
*/
|
||||||
|
var tabsTrayRewriteEnabled: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable the Top Sites to Compose rewrite.
|
||||||
|
*/
|
||||||
|
var composeTopSitesEnabled: Boolean
|
||||||
|
|
||||||
|
fun applyFlagUpdates()
|
||||||
|
|
||||||
|
fun resetAllFeatureFlags()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val settings = InstrumentationRegistry.getInstrumentation().targetContext.settings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All "Enhanced Tracking Protection" modes.
|
||||||
|
*/
|
||||||
|
enum class ETPPolicy {
|
||||||
|
STANDARD,
|
||||||
|
STRICT,
|
||||||
|
CUSTOM,
|
||||||
|
}
|
|
@ -0,0 +1,177 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.helpers
|
||||||
|
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.ext.getPreferenceKey
|
||||||
|
import org.mozilla.fenix.helpers.ETPPolicy.CUSTOM
|
||||||
|
import org.mozilla.fenix.helpers.ETPPolicy.STANDARD
|
||||||
|
import org.mozilla.fenix.helpers.ETPPolicy.STRICT
|
||||||
|
import org.mozilla.fenix.helpers.FeatureSettingsHelper.Companion.settings
|
||||||
|
import org.mozilla.fenix.helpers.TestHelper.appContext
|
||||||
|
import org.mozilla.fenix.onboarding.FenixOnboarding
|
||||||
|
import org.mozilla.fenix.utils.Settings
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for querying the status and modifying various features and settings in the application.
|
||||||
|
*/
|
||||||
|
class FeatureSettingsHelperDelegate() : FeatureSettingsHelper {
|
||||||
|
/**
|
||||||
|
* The current feature flags used inside the app before the tests start.
|
||||||
|
* These will be restored when the tests end.
|
||||||
|
*/
|
||||||
|
private val initialFeatureFlags = FeatureFlags(
|
||||||
|
isHomeOnboardingDialogEnabled = settings.showHomeOnboardingDialog,
|
||||||
|
homeOnboardingDialogVersion = getHomeOnboardingVersion(),
|
||||||
|
isPocketEnabled = settings.showPocketRecommendationsFeature,
|
||||||
|
isJumpBackInCFREnabled = settings.shouldShowJumpBackInCFR,
|
||||||
|
isRecentTabsFeatureEnabled = settings.showRecentTabsFeature,
|
||||||
|
isRecentlyVisitedFeatureEnabled = settings.historyMetadataUIFeature,
|
||||||
|
isPWAsPromptEnabled = !settings.userKnowsAboutPwas,
|
||||||
|
isTCPCFREnabled = settings.shouldShowTotalCookieProtectionCFR,
|
||||||
|
isWallpaperOnboardingEnabled = settings.showWallpaperOnboarding,
|
||||||
|
isDeleteSitePermissionsEnabled = settings.deleteSitePermissions,
|
||||||
|
isCookieBannerReductionDialogEnabled = !settings.userOptOutOfReEngageCookieBannerDialog,
|
||||||
|
isOpenInAppBannerEnabled = settings.shouldShowOpenInAppBanner,
|
||||||
|
etpPolicy = getETPPolicy(settings),
|
||||||
|
tabsTrayRewriteEnabled = settings.enableTabsTrayToCompose,
|
||||||
|
composeTopSitesEnabled = settings.enableComposeTopSites,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current feature flags updated in tests.
|
||||||
|
*/
|
||||||
|
private var updatedFeatureFlags = initialFeatureFlags.copy()
|
||||||
|
|
||||||
|
override var isHomeOnboardingDialogEnabled: Boolean
|
||||||
|
get() = updatedFeatureFlags.isHomeOnboardingDialogEnabled &&
|
||||||
|
FenixOnboarding(appContext).userHasBeenOnboarded()
|
||||||
|
set(value) {
|
||||||
|
updatedFeatureFlags.isHomeOnboardingDialogEnabled = value
|
||||||
|
updatedFeatureFlags.homeOnboardingDialogVersion = when (value) {
|
||||||
|
true -> FenixOnboarding.CURRENT_ONBOARDING_VERSION
|
||||||
|
false -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var isPocketEnabled: Boolean by updatedFeatureFlags::isPocketEnabled
|
||||||
|
override var isJumpBackInCFREnabled: Boolean by updatedFeatureFlags::isJumpBackInCFREnabled
|
||||||
|
override var isWallpaperOnboardingEnabled: Boolean by updatedFeatureFlags::isWallpaperOnboardingEnabled
|
||||||
|
override var isRecentTabsFeatureEnabled: Boolean by updatedFeatureFlags::isRecentTabsFeatureEnabled
|
||||||
|
override var isRecentlyVisitedFeatureEnabled: Boolean by updatedFeatureFlags::isRecentlyVisitedFeatureEnabled
|
||||||
|
override var isPWAsPromptEnabled: Boolean by updatedFeatureFlags::isPWAsPromptEnabled
|
||||||
|
override var isTCPCFREnabled: Boolean by updatedFeatureFlags::isTCPCFREnabled
|
||||||
|
override var isCookieBannerReductionDialogEnabled: Boolean by updatedFeatureFlags::isCookieBannerReductionDialogEnabled
|
||||||
|
override var isOpenInAppBannerEnabled: Boolean by updatedFeatureFlags::isOpenInAppBannerEnabled
|
||||||
|
override var etpPolicy: ETPPolicy by updatedFeatureFlags::etpPolicy
|
||||||
|
override var tabsTrayRewriteEnabled: Boolean by updatedFeatureFlags::tabsTrayRewriteEnabled
|
||||||
|
override var composeTopSitesEnabled: Boolean by updatedFeatureFlags::composeTopSitesEnabled
|
||||||
|
|
||||||
|
override fun applyFlagUpdates() {
|
||||||
|
applyFeatureFlags(updatedFeatureFlags)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resetAllFeatureFlags() {
|
||||||
|
applyFeatureFlags(initialFeatureFlags)
|
||||||
|
}
|
||||||
|
|
||||||
|
override var isDeleteSitePermissionsEnabled: Boolean by updatedFeatureFlags::isDeleteSitePermissionsEnabled
|
||||||
|
|
||||||
|
private fun applyFeatureFlags(featureFlags: FeatureFlags) {
|
||||||
|
settings.showHomeOnboardingDialog = featureFlags.isHomeOnboardingDialogEnabled
|
||||||
|
setHomeOnboardingVersion(featureFlags.homeOnboardingDialogVersion)
|
||||||
|
settings.showPocketRecommendationsFeature = featureFlags.isPocketEnabled
|
||||||
|
settings.shouldShowJumpBackInCFR = featureFlags.isJumpBackInCFREnabled
|
||||||
|
settings.showRecentTabsFeature = featureFlags.isRecentTabsFeatureEnabled
|
||||||
|
settings.historyMetadataUIFeature = featureFlags.isRecentlyVisitedFeatureEnabled
|
||||||
|
settings.userKnowsAboutPwas = !featureFlags.isPWAsPromptEnabled
|
||||||
|
settings.shouldShowTotalCookieProtectionCFR = featureFlags.isTCPCFREnabled
|
||||||
|
settings.showWallpaperOnboarding = featureFlags.isWallpaperOnboardingEnabled
|
||||||
|
settings.deleteSitePermissions = featureFlags.isDeleteSitePermissionsEnabled
|
||||||
|
settings.userOptOutOfReEngageCookieBannerDialog = !featureFlags.isCookieBannerReductionDialogEnabled
|
||||||
|
settings.shouldShowOpenInAppBanner = featureFlags.isOpenInAppBannerEnabled
|
||||||
|
settings.enableTabsTrayToCompose = featureFlags.tabsTrayRewriteEnabled
|
||||||
|
settings.enableComposeTopSites = featureFlags.composeTopSitesEnabled
|
||||||
|
setETPPolicy(featureFlags.etpPolicy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class FeatureFlags(
|
||||||
|
var isHomeOnboardingDialogEnabled: Boolean,
|
||||||
|
var homeOnboardingDialogVersion: Int,
|
||||||
|
var isPocketEnabled: Boolean,
|
||||||
|
var isJumpBackInCFREnabled: Boolean,
|
||||||
|
var isRecentTabsFeatureEnabled: Boolean,
|
||||||
|
var isRecentlyVisitedFeatureEnabled: Boolean,
|
||||||
|
var isPWAsPromptEnabled: Boolean,
|
||||||
|
var isTCPCFREnabled: Boolean,
|
||||||
|
var isWallpaperOnboardingEnabled: Boolean,
|
||||||
|
var isDeleteSitePermissionsEnabled: Boolean,
|
||||||
|
var isCookieBannerReductionDialogEnabled: Boolean,
|
||||||
|
var isOpenInAppBannerEnabled: Boolean,
|
||||||
|
var etpPolicy: ETPPolicy,
|
||||||
|
var tabsTrayRewriteEnabled: Boolean,
|
||||||
|
var composeTopSitesEnabled: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun getETPPolicy(settings: Settings): ETPPolicy {
|
||||||
|
return when {
|
||||||
|
settings.useStrictTrackingProtection -> STRICT
|
||||||
|
settings.useCustomTrackingProtection -> CUSTOM
|
||||||
|
else -> STANDARD
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setETPPolicy(policy: ETPPolicy) {
|
||||||
|
when (policy) {
|
||||||
|
STRICT -> settings.setStrictETP()
|
||||||
|
// The following two cases update ETP in the same way "setStrictETP" does.
|
||||||
|
STANDARD -> {
|
||||||
|
settings.preferences.edit()
|
||||||
|
.putBoolean(
|
||||||
|
appContext.getPreferenceKey(R.string.pref_key_tracking_protection_strict_default),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.putBoolean(
|
||||||
|
appContext.getPreferenceKey(R.string.pref_key_tracking_protection_custom_option),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.putBoolean(
|
||||||
|
appContext.getPreferenceKey(R.string.pref_key_tracking_protection_standard_option),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
CUSTOM -> {
|
||||||
|
settings.preferences.edit()
|
||||||
|
.putBoolean(
|
||||||
|
appContext.getPreferenceKey(R.string.pref_key_tracking_protection_strict_default),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.putBoolean(
|
||||||
|
appContext.getPreferenceKey(R.string.pref_key_tracking_protection_standard_option),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.putBoolean(
|
||||||
|
appContext.getPreferenceKey(R.string.pref_key_tracking_protection_custom_option),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getHomeOnboardingVersion(): Int {
|
||||||
|
return FenixOnboarding(appContext)
|
||||||
|
.preferences
|
||||||
|
.getInt(FenixOnboarding.LAST_VERSION_ONBOARDING_KEY, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setHomeOnboardingVersion(version: Int) {
|
||||||
|
FenixOnboarding(appContext)
|
||||||
|
.preferences.edit()
|
||||||
|
.putInt(FenixOnboarding.LAST_VERSION_ONBOARDING_KEY, version)
|
||||||
|
.commit()
|
||||||
|
}
|
|
@ -0,0 +1,312 @@
|
||||||
|
/* 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 org.mozilla.fenix.helpers
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.view.ViewConfiguration.getLongPressTimeout
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.test.espresso.intent.rule.IntentsTestRule
|
||||||
|
import androidx.test.rule.ActivityTestRule
|
||||||
|
import androidx.test.uiautomator.UiSelector
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.mozilla.fenix.HomeActivity
|
||||||
|
import org.mozilla.fenix.helpers.FeatureSettingsHelper.Companion.settings
|
||||||
|
import org.mozilla.fenix.helpers.TestHelper.appContext
|
||||||
|
import org.mozilla.fenix.helpers.TestHelper.mDevice
|
||||||
|
import org.mozilla.fenix.onboarding.FenixOnboarding
|
||||||
|
|
||||||
|
typealias HomeActivityComposeTestRule = AndroidComposeTestRule<out TestRule, HomeActivity>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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),
|
||||||
|
FeatureSettingsHelper by FeatureSettingsHelperDelegate() {
|
||||||
|
|
||||||
|
// Using a secondary constructor allows us to easily delegate the settings to FeatureSettingsHelperDelegate.
|
||||||
|
// Otherwise if wanting to use the same names we would have to override these settings in the primary
|
||||||
|
// constructor and in that elide the FeatureSettingsHelperDelegate.
|
||||||
|
constructor(
|
||||||
|
initialTouchMode: Boolean = false,
|
||||||
|
launchActivity: Boolean = true,
|
||||||
|
skipOnboarding: Boolean = false,
|
||||||
|
isHomeOnboardingDialogEnabled: Boolean = settings.showHomeOnboardingDialog &&
|
||||||
|
FenixOnboarding(appContext).userHasBeenOnboarded(),
|
||||||
|
isPocketEnabled: Boolean = settings.showPocketRecommendationsFeature,
|
||||||
|
isJumpBackInCFREnabled: Boolean = settings.shouldShowJumpBackInCFR,
|
||||||
|
isRecentTabsFeatureEnabled: Boolean = settings.showRecentTabsFeature,
|
||||||
|
isRecentlyVisitedFeatureEnabled: Boolean = settings.historyMetadataUIFeature,
|
||||||
|
isPWAsPromptEnabled: Boolean = !settings.userKnowsAboutPwas,
|
||||||
|
isTCPCFREnabled: Boolean = settings.shouldShowTotalCookieProtectionCFR,
|
||||||
|
isWallpaperOnboardingEnabled: Boolean = settings.showWallpaperOnboarding,
|
||||||
|
isDeleteSitePermissionsEnabled: Boolean = settings.deleteSitePermissions,
|
||||||
|
isCookieBannerReductionDialogEnabled: Boolean = !settings.userOptOutOfReEngageCookieBannerDialog,
|
||||||
|
isOpenInAppBannerEnabled: Boolean = settings.shouldShowOpenInAppBanner,
|
||||||
|
etpPolicy: ETPPolicy = getETPPolicy(settings),
|
||||||
|
tabsTrayRewriteEnabled: Boolean = false,
|
||||||
|
composeTopSitesEnabled: Boolean = false,
|
||||||
|
) : this(initialTouchMode, launchActivity, skipOnboarding) {
|
||||||
|
this.isHomeOnboardingDialogEnabled = isHomeOnboardingDialogEnabled
|
||||||
|
this.isPocketEnabled = isPocketEnabled
|
||||||
|
this.isJumpBackInCFREnabled = isJumpBackInCFREnabled
|
||||||
|
this.isRecentTabsFeatureEnabled = isRecentTabsFeatureEnabled
|
||||||
|
this.isRecentlyVisitedFeatureEnabled = isRecentlyVisitedFeatureEnabled
|
||||||
|
this.isPWAsPromptEnabled = isPWAsPromptEnabled
|
||||||
|
this.isTCPCFREnabled = isTCPCFREnabled
|
||||||
|
this.isWallpaperOnboardingEnabled = isWallpaperOnboardingEnabled
|
||||||
|
this.isDeleteSitePermissionsEnabled = isDeleteSitePermissionsEnabled
|
||||||
|
this.isCookieBannerReductionDialogEnabled = isCookieBannerReductionDialogEnabled
|
||||||
|
this.isOpenInAppBannerEnabled = isOpenInAppBannerEnabled
|
||||||
|
this.etpPolicy = etpPolicy
|
||||||
|
this.tabsTrayRewriteEnabled = tabsTrayRewriteEnabled
|
||||||
|
this.composeTopSitesEnabled = composeTopSitesEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update settings after the activity was created.
|
||||||
|
*/
|
||||||
|
fun applySettingsExceptions(settings: (FeatureSettingsHelper) -> Unit) {
|
||||||
|
FeatureSettingsHelperDelegate().also {
|
||||||
|
settings(this)
|
||||||
|
applyFlagUpdates()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val longTapUserPreference = getLongPressTimeout()
|
||||||
|
|
||||||
|
override fun beforeActivityLaunched() {
|
||||||
|
super.beforeActivityLaunched()
|
||||||
|
setLongTapTimeout(3000)
|
||||||
|
applyFlagUpdates()
|
||||||
|
if (skipOnboarding) { skipOnboardingBeforeLaunch() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun afterActivityFinished() {
|
||||||
|
super.afterActivityFinished()
|
||||||
|
setLongTapTimeout(longTapUserPreference)
|
||||||
|
resetAllFeatureFlags()
|
||||||
|
closeNotificationShade()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Create a new instance of [HomeActivityTestRule] which by default will disable specific
|
||||||
|
* app features that would otherwise negatively impact most tests.
|
||||||
|
*
|
||||||
|
* The disabled features are:
|
||||||
|
* - the Jump back in CFR,
|
||||||
|
* - the Total Cookie Protection CFR,
|
||||||
|
* - the PWA prompt dialog,
|
||||||
|
* - the wallpaper onboarding.
|
||||||
|
*/
|
||||||
|
fun withDefaultSettingsOverrides(
|
||||||
|
initialTouchMode: Boolean = false,
|
||||||
|
launchActivity: Boolean = true,
|
||||||
|
skipOnboarding: Boolean = false,
|
||||||
|
tabsTrayRewriteEnabled: Boolean = false,
|
||||||
|
composeTopSitesEnabled: Boolean = false,
|
||||||
|
) = HomeActivityTestRule(
|
||||||
|
initialTouchMode = initialTouchMode,
|
||||||
|
launchActivity = launchActivity,
|
||||||
|
skipOnboarding = skipOnboarding,
|
||||||
|
tabsTrayRewriteEnabled = tabsTrayRewriteEnabled,
|
||||||
|
isJumpBackInCFREnabled = false,
|
||||||
|
isPWAsPromptEnabled = false,
|
||||||
|
isTCPCFREnabled = false,
|
||||||
|
isWallpaperOnboardingEnabled = false,
|
||||||
|
isCookieBannerReductionDialogEnabled = false,
|
||||||
|
isOpenInAppBannerEnabled = false,
|
||||||
|
composeTopSitesEnabled = composeTopSitesEnabled,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 internal constructor(
|
||||||
|
initialTouchMode: Boolean = false,
|
||||||
|
launchActivity: Boolean = true,
|
||||||
|
private val skipOnboarding: Boolean = false,
|
||||||
|
) : IntentsTestRule<HomeActivity>(HomeActivity::class.java, initialTouchMode, launchActivity),
|
||||||
|
FeatureSettingsHelper by FeatureSettingsHelperDelegate() {
|
||||||
|
// Using a secondary constructor allows us to easily delegate the settings to FeatureSettingsHelperDelegate.
|
||||||
|
// Otherwise if wanting to use the same names we would have to override these settings in the primary
|
||||||
|
// constructor and in that elide the FeatureSettingsHelperDelegate.
|
||||||
|
constructor(
|
||||||
|
initialTouchMode: Boolean = false,
|
||||||
|
launchActivity: Boolean = true,
|
||||||
|
skipOnboarding: Boolean = false,
|
||||||
|
isHomeOnboardingDialogEnabled: Boolean = settings.showHomeOnboardingDialog &&
|
||||||
|
FenixOnboarding(appContext).userHasBeenOnboarded(),
|
||||||
|
isPocketEnabled: Boolean = settings.showPocketRecommendationsFeature,
|
||||||
|
isJumpBackInCFREnabled: Boolean = settings.shouldShowJumpBackInCFR,
|
||||||
|
isRecentTabsFeatureEnabled: Boolean = settings.showRecentTabsFeature,
|
||||||
|
isRecentlyVisitedFeatureEnabled: Boolean = settings.historyMetadataUIFeature,
|
||||||
|
isPWAsPromptEnabled: Boolean = !settings.userKnowsAboutPwas,
|
||||||
|
isTCPCFREnabled: Boolean = settings.shouldShowTotalCookieProtectionCFR,
|
||||||
|
isWallpaperOnboardingEnabled: Boolean = settings.showWallpaperOnboarding,
|
||||||
|
isDeleteSitePermissionsEnabled: Boolean = settings.deleteSitePermissions,
|
||||||
|
isCookieBannerReductionDialogEnabled: Boolean = !settings.userOptOutOfReEngageCookieBannerDialog,
|
||||||
|
isOpenInAppBannerEnabled: Boolean = settings.shouldShowOpenInAppBanner,
|
||||||
|
etpPolicy: ETPPolicy = getETPPolicy(settings),
|
||||||
|
tabsTrayRewriteEnabled: Boolean = false,
|
||||||
|
composeTopSitesEnabled: Boolean = false,
|
||||||
|
) : this(initialTouchMode, launchActivity, skipOnboarding) {
|
||||||
|
this.isHomeOnboardingDialogEnabled = isHomeOnboardingDialogEnabled
|
||||||
|
this.isPocketEnabled = isPocketEnabled
|
||||||
|
this.isJumpBackInCFREnabled = isJumpBackInCFREnabled
|
||||||
|
this.isRecentTabsFeatureEnabled = isRecentTabsFeatureEnabled
|
||||||
|
this.isRecentlyVisitedFeatureEnabled = isRecentlyVisitedFeatureEnabled
|
||||||
|
this.isPWAsPromptEnabled = isPWAsPromptEnabled
|
||||||
|
this.isTCPCFREnabled = isTCPCFREnabled
|
||||||
|
this.isWallpaperOnboardingEnabled = isWallpaperOnboardingEnabled
|
||||||
|
this.isDeleteSitePermissionsEnabled = isDeleteSitePermissionsEnabled
|
||||||
|
this.isCookieBannerReductionDialogEnabled = isCookieBannerReductionDialogEnabled
|
||||||
|
this.isOpenInAppBannerEnabled = isOpenInAppBannerEnabled
|
||||||
|
this.etpPolicy = etpPolicy
|
||||||
|
this.tabsTrayRewriteEnabled = tabsTrayRewriteEnabled
|
||||||
|
this.composeTopSitesEnabled = composeTopSitesEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
private val longTapUserPreference = getLongPressTimeout()
|
||||||
|
|
||||||
|
private lateinit var intent: Intent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update settings after the activity was created.
|
||||||
|
*/
|
||||||
|
fun applySettingsExceptions(settings: (FeatureSettingsHelper) -> Unit) {
|
||||||
|
FeatureSettingsHelperDelegate().apply {
|
||||||
|
settings(this)
|
||||||
|
applyFlagUpdates()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getActivityIntent(): Intent? {
|
||||||
|
return if (this::intent.isInitialized) {
|
||||||
|
this.intent
|
||||||
|
} else {
|
||||||
|
super.getActivityIntent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun withIntent(intent: Intent): HomeActivityIntentTestRule {
|
||||||
|
this.intent = intent
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun beforeActivityLaunched() {
|
||||||
|
super.beforeActivityLaunched()
|
||||||
|
setLongTapTimeout(3000)
|
||||||
|
applyFlagUpdates()
|
||||||
|
if (skipOnboarding) { skipOnboardingBeforeLaunch() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun afterActivityFinished() {
|
||||||
|
super.afterActivityFinished()
|
||||||
|
setLongTapTimeout(longTapUserPreference)
|
||||||
|
closeNotificationShade()
|
||||||
|
resetAllFeatureFlags()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the settings values from when this rule was first instantiated to account for any changes
|
||||||
|
* done while running the tests.
|
||||||
|
* Useful in the scenario about the activity being restarted which would otherwise set the initial
|
||||||
|
* settings and override any changes made in the meantime.
|
||||||
|
*/
|
||||||
|
fun updateCachedSettings() {
|
||||||
|
isHomeOnboardingDialogEnabled =
|
||||||
|
settings.showHomeOnboardingDialog && FenixOnboarding(appContext).userHasBeenOnboarded()
|
||||||
|
isPocketEnabled = settings.showPocketRecommendationsFeature
|
||||||
|
isJumpBackInCFREnabled = settings.shouldShowJumpBackInCFR
|
||||||
|
isRecentTabsFeatureEnabled = settings.showRecentTabsFeature
|
||||||
|
isRecentlyVisitedFeatureEnabled = settings.historyMetadataUIFeature
|
||||||
|
isPWAsPromptEnabled = !settings.userKnowsAboutPwas
|
||||||
|
isTCPCFREnabled = settings.shouldShowTotalCookieProtectionCFR
|
||||||
|
isWallpaperOnboardingEnabled = settings.showWallpaperOnboarding
|
||||||
|
isDeleteSitePermissionsEnabled = settings.deleteSitePermissions
|
||||||
|
isCookieBannerReductionDialogEnabled = !settings.userOptOutOfReEngageCookieBannerDialog
|
||||||
|
isOpenInAppBannerEnabled = settings.shouldShowOpenInAppBanner
|
||||||
|
etpPolicy = getETPPolicy(settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Create a new instance of [HomeActivityIntentTestRule] which by default will disable specific
|
||||||
|
* app features that would otherwise negatively impact most tests.
|
||||||
|
*
|
||||||
|
* The disabled features are:
|
||||||
|
* - the Jump back in CFR,
|
||||||
|
* - the Total Cookie Protection CFR,
|
||||||
|
* - the PWA prompt dialog,
|
||||||
|
* - the wallpaper onboarding.
|
||||||
|
*/
|
||||||
|
fun withDefaultSettingsOverrides(
|
||||||
|
initialTouchMode: Boolean = false,
|
||||||
|
launchActivity: Boolean = true,
|
||||||
|
skipOnboarding: Boolean = false,
|
||||||
|
tabsTrayRewriteEnabled: Boolean = false,
|
||||||
|
composeTopSitesEnabled: Boolean = false,
|
||||||
|
) = HomeActivityIntentTestRule(
|
||||||
|
initialTouchMode = initialTouchMode,
|
||||||
|
launchActivity = launchActivity,
|
||||||
|
skipOnboarding = skipOnboarding,
|
||||||
|
tabsTrayRewriteEnabled = tabsTrayRewriteEnabled,
|
||||||
|
isJumpBackInCFREnabled = false,
|
||||||
|
isPWAsPromptEnabled = false,
|
||||||
|
isTCPCFREnabled = false,
|
||||||
|
isWallpaperOnboardingEnabled = false,
|
||||||
|
isCookieBannerReductionDialogEnabled = false,
|
||||||
|
isOpenInAppBannerEnabled = false,
|
||||||
|
composeTopSitesEnabled = composeTopSitesEnabled,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
FenixOnboarding(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 org.mozilla.fenix.helpers
|
||||||
|
|
||||||
|
import androidx.test.espresso.IdlingRegistry
|
||||||
|
import androidx.test.rule.ActivityTestRule
|
||||||
|
import org.mozilla.fenix.HomeActivity
|
||||||
|
import org.mozilla.fenix.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,117 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.helpers
|
||||||
|
|
||||||
|
import androidx.test.uiautomator.UiObject
|
||||||
|
import androidx.test.uiautomator.UiSelector
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
|
||||||
|
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
|
||||||
|
import org.mozilla.fenix.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 itemWithText(itemText: String) =
|
||||||
|
mDevice.findObject(UiSelector().text(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 itemWithResIdContainingText(resourceId: String, text: String) =
|
||||||
|
mDevice.findObject(UiSelector().resourceId(resourceId).textContains(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(waitingTimeShort))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertItemContainingTextExists(vararg appItems: UiObject, exists: Boolean = true) {
|
||||||
|
for (appItem in appItems) {
|
||||||
|
if (exists) {
|
||||||
|
assertTrue(appItem.waitForExists(waitingTime))
|
||||||
|
} else {
|
||||||
|
assertFalse(appItem.waitForExists(waitingTimeShort))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertItemWithDescriptionExists(vararg appItems: UiObject, exists: Boolean = true) {
|
||||||
|
for (appItem in appItems) {
|
||||||
|
if (exists) {
|
||||||
|
assertTrue(appItem.waitForExists(waitingTime))
|
||||||
|
} else {
|
||||||
|
assertFalse(appItem.waitForExists(waitingTimeShort))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, exists: Boolean = true) {
|
||||||
|
for (appItem in appItems) {
|
||||||
|
if (exists) {
|
||||||
|
assertTrue(appItem.waitForExists(waitingTime))
|
||||||
|
} else {
|
||||||
|
assertFalse(appItem.waitForExists(waitingTimeShort))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertItemIsEnabledAndVisible(vararg appItems: UiObject) {
|
||||||
|
for (appItem in appItems) {
|
||||||
|
assertTrue(appItem.waitForExists(waitingTime) && appItem.isEnabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 org.mozilla.fenix.helpers
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.test.espresso.ViewInteraction
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||||
|
import androidx.test.espresso.matcher.BoundedMatcher
|
||||||
|
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 org.mozilla.fenix.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
|
||||||
|
}
|
||||||
|
|
||||||
|
fun atPosition(position: Int, itemMatcher: Matcher<View?>): Matcher<View?>? {
|
||||||
|
return object : BoundedMatcher<View?, RecyclerView>(
|
||||||
|
RecyclerView::class.java,
|
||||||
|
) {
|
||||||
|
override fun describeTo(description: Description) {
|
||||||
|
description.appendText("has item at position $position: ")
|
||||||
|
itemMatcher.describeTo(description)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun matchesSafely(view: RecyclerView): Boolean {
|
||||||
|
val viewHolder = view.findViewHolderForAdapterPosition(position)
|
||||||
|
?: // has no item on such position
|
||||||
|
return false
|
||||||
|
return itemMatcher.matches(viewHolder.itemView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.helpers
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import mozilla.appservices.places.BookmarkRoot
|
||||||
|
import mozilla.components.browser.icons.IconRequest
|
||||||
|
import mozilla.components.browser.icons.generator.DefaultIconGenerator
|
||||||
|
import mozilla.components.browser.state.search.SearchEngine
|
||||||
|
import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
|
||||||
|
import mozilla.components.browser.storage.sync.PlacesHistoryStorage
|
||||||
|
import mozilla.components.concept.storage.PageVisit
|
||||||
|
import mozilla.components.concept.storage.VisitType
|
||||||
|
import mozilla.components.feature.search.ext.createSearchEngine
|
||||||
|
import okhttp3.mockwebserver.MockWebServer
|
||||||
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.helpers.TestHelper.appContext
|
||||||
|
import org.mozilla.fenix.search.SearchEngineSource.None.searchEngine
|
||||||
|
|
||||||
|
object MockBrowserDataHelper {
|
||||||
|
val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new bookmark item, visible in the Bookmarks folder.
|
||||||
|
*
|
||||||
|
* @param url The URL of the bookmark item to add. URLs should use the "https://example.com" format.
|
||||||
|
* @param title The title of the bookmark item to add.
|
||||||
|
* @param position Example for the position param: 1u, 2u, etc.
|
||||||
|
*/
|
||||||
|
fun createBookmarkItem(url: String, title: String, position: UInt?) {
|
||||||
|
runBlocking {
|
||||||
|
PlacesBookmarksStorage(context)
|
||||||
|
.addItem(
|
||||||
|
BookmarkRoot.Mobile.id,
|
||||||
|
url,
|
||||||
|
title,
|
||||||
|
position,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new history item, visible in the History folder.
|
||||||
|
*
|
||||||
|
* @param url The URL of the history item to add. URLs should use the "https://example.com" format.
|
||||||
|
*/
|
||||||
|
fun createHistoryItem(url: String) {
|
||||||
|
runBlocking {
|
||||||
|
PlacesHistoryStorage(appContext)
|
||||||
|
.recordVisit(
|
||||||
|
url,
|
||||||
|
PageVisit(VisitType.LINK),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new tab with a webpage, also visible in the History folder.
|
||||||
|
*
|
||||||
|
* URLs should use the "https://example.com" format.
|
||||||
|
*/
|
||||||
|
fun createTabItem(url: String) {
|
||||||
|
runBlocking {
|
||||||
|
appContext.components.useCases.tabsUseCases.addTab(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers a search for the provided search term in a new tab.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
fun createSearchHistory(searchTerm: String) {
|
||||||
|
appContext.components.useCases.searchUseCases.newTabSearch.invoke(searchTerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new custom search engine object.
|
||||||
|
*
|
||||||
|
* @param mockWebServer The mockWebServer instance.
|
||||||
|
* @param searchEngineName The name of the new search engine.
|
||||||
|
*/
|
||||||
|
private fun createCustomSearchEngine(mockWebServer: MockWebServer, searchEngineName: String): SearchEngine {
|
||||||
|
val searchString =
|
||||||
|
"http://localhost:${mockWebServer.port}/pages/searchResults.html?search={searchTerms}"
|
||||||
|
return createSearchEngine(
|
||||||
|
name = searchEngineName,
|
||||||
|
url = searchString,
|
||||||
|
icon = DefaultIconGenerator().generate(appContext, IconRequest(searchString)).bitmap,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new custom search engine to the apps Search Engines list.
|
||||||
|
*
|
||||||
|
* @param searchEngine Use createCustomSearchEngine method to create one.
|
||||||
|
*/
|
||||||
|
fun addCustomSearchEngine(mockWebServer: MockWebServer, searchEngineName: String) {
|
||||||
|
val searchEngine = createCustomSearchEngine(mockWebServer, searchEngineName)
|
||||||
|
|
||||||
|
appContext.components.useCases.searchUseCases.addSearchEngine(searchEngine)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds and selects as default a new custom search engine to the apps Search Engines list.
|
||||||
|
*
|
||||||
|
* @param searchEngine Use createCustomSearchEngine method to create one.
|
||||||
|
*/
|
||||||
|
fun setCustomSearchEngine(mockWebServer: MockWebServer, searchEngineName: String) {
|
||||||
|
val searchEngine = createCustomSearchEngine(mockWebServer, searchEngineName)
|
||||||
|
|
||||||
|
with(appContext.components.useCases.searchUseCases) {
|
||||||
|
addSearchEngine(searchEngine)
|
||||||
|
selectSearchEngine(searchEngine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 org.mozilla.fenix.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 org.mozilla.fenix.helpers.TestHelper.mDevice
|
||||||
|
import java.util.Date
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
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 org.mozilla.fenix.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 org.mozilla.fenix.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 [org.mozilla.fenix.ui.BookmarksTest.verifyBookmarkButtonTest]
|
||||||
|
*/
|
||||||
|
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 org.mozilla.fenix.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,109 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.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 org.mozilla.fenix.components.PermissionStorage
|
||||||
|
import org.mozilla.fenix.helpers.IdlingResourceHelper.unregisterAllIdlingResources
|
||||||
|
import org.mozilla.fenix.helpers.TestHelper.appContext
|
||||||
|
import org.mozilla.fenix.helpers.TestHelper.setNetworkEnabled
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
setNetworkEnabled(true)
|
||||||
|
unregisterAllIdlingResources()
|
||||||
|
runBlocking {
|
||||||
|
permissionStorage.deleteAllSitePermissions()
|
||||||
|
}
|
||||||
|
if (i == retryCount) {
|
||||||
|
throw t
|
||||||
|
}
|
||||||
|
} catch (t: AssertionFailedError) {
|
||||||
|
unregisterAllIdlingResources()
|
||||||
|
runBlocking {
|
||||||
|
permissionStorage.deleteAllSitePermissions()
|
||||||
|
}
|
||||||
|
if (i == retryCount) {
|
||||||
|
throw t
|
||||||
|
}
|
||||||
|
} catch (t: UiObjectNotFoundException) {
|
||||||
|
setNetworkEnabled(true)
|
||||||
|
unregisterAllIdlingResources()
|
||||||
|
runBlocking {
|
||||||
|
permissionStorage.deleteAllSitePermissions()
|
||||||
|
}
|
||||||
|
if (i == retryCount) {
|
||||||
|
throw t
|
||||||
|
}
|
||||||
|
} catch (t: NoMatchingViewException) {
|
||||||
|
setNetworkEnabled(true)
|
||||||
|
unregisterAllIdlingResources()
|
||||||
|
runBlocking {
|
||||||
|
permissionStorage.deleteAllSitePermissions()
|
||||||
|
}
|
||||||
|
if (i == retryCount) {
|
||||||
|
throw t
|
||||||
|
}
|
||||||
|
} catch (t: IdlingResourceTimeoutException) {
|
||||||
|
setNetworkEnabled(true)
|
||||||
|
unregisterAllIdlingResources()
|
||||||
|
runBlocking {
|
||||||
|
permissionStorage.deleteAllSitePermissions()
|
||||||
|
}
|
||||||
|
if (i == retryCount) {
|
||||||
|
throw t
|
||||||
|
}
|
||||||
|
} catch (t: RuntimeException) {
|
||||||
|
setNetworkEnabled(true)
|
||||||
|
unregisterAllIdlingResources()
|
||||||
|
runBlocking {
|
||||||
|
permissionStorage.deleteAllSitePermissions()
|
||||||
|
}
|
||||||
|
if (i == retryCount) {
|
||||||
|
throw t
|
||||||
|
}
|
||||||
|
} catch (t: NullPointerException) {
|
||||||
|
setNetworkEnabled(true)
|
||||||
|
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 org.mozilla.fenix.helpers
|
||||||
|
|
||||||
|
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 java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 [org.mozilla.fenix.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 org.mozilla.fenix.helpers
|
||||||
|
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.espresso.IdlingResource
|
||||||
|
import mozilla.components.browser.state.selector.selectedTab
|
||||||
|
import org.mozilla.fenix.FenixApplication
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<FenixApplication>()
|
||||||
|
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,149 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.helpers
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import okhttp3.mockwebserver.MockWebServer
|
||||||
|
import org.mozilla.fenix.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 getPdfFormAsset(server: MockWebServer): TestAsset {
|
||||||
|
val url = server.url("resources/pdfForm.pdf").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 getHTMLControlsFormAsset(server: MockWebServer): TestAsset {
|
||||||
|
val url = server.url("pages/htmlControls.html").toString().toUri()!!
|
||||||
|
|
||||||
|
return TestAsset(url, "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getExternalLinksAsset(server: MockWebServer): TestAsset {
|
||||||
|
val url = server.url("pages/externalLinks.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 getMutedVideoPageAsset(server: MockWebServer): TestAsset {
|
||||||
|
val url = server.url("pages/mutedVideoPage.html").toString().toUri()!!
|
||||||
|
val title = "Muted_Video_Test_Page"
|
||||||
|
val content = "Page content: muted 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,527 @@
|
||||||
|
/* 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 org.mozilla.fenix.helpers
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.storage.StorageManager
|
||||||
|
import android.os.storage.StorageVolume
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
|
import androidx.test.espresso.Espresso
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.IdlingRegistry
|
||||||
|
import androidx.test.espresso.IdlingResource
|
||||||
|
import androidx.test.espresso.action.ViewActions.longClick
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions
|
||||||
|
import androidx.test.espresso.intent.Intents
|
||||||
|
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.rule.ActivityTestRule
|
||||||
|
import androidx.test.runner.permission.PermissionRequester
|
||||||
|
import androidx.test.uiautomator.By
|
||||||
|
import androidx.test.uiautomator.UiDevice
|
||||||
|
import androidx.test.uiautomator.UiObject
|
||||||
|
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.browser.state.state.availableSearchEngines
|
||||||
|
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.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.mozilla.fenix.Config
|
||||||
|
import org.mozilla.fenix.HomeActivity
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.customtabs.ExternalAppBrowserActivity
|
||||||
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.helpers.Constants.PackageName.YOUTUBE_APP
|
||||||
|
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
|
||||||
|
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
|
||||||
|
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
|
||||||
|
import org.mozilla.fenix.helpers.ext.waitNotNull
|
||||||
|
import org.mozilla.fenix.helpers.idlingresource.NetworkConnectionIdlingResource
|
||||||
|
import org.mozilla.fenix.ui.robots.BrowserRobot
|
||||||
|
import org.mozilla.fenix.ui.robots.clickPageObject
|
||||||
|
import org.mozilla.fenix.utils.IntentUtils
|
||||||
|
import org.mozilla.gecko.util.ThreadUtils
|
||||||
|
import java.io.File
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
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) {
|
||||||
|
updateCachedSettings()
|
||||||
|
finishActivity()
|
||||||
|
mDevice.waitForIdle()
|
||||||
|
launchActivity(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun closeApp(activity: HomeActivityIntentTestRule) =
|
||||||
|
activity.activity.finishAndRemoveTask()
|
||||||
|
|
||||||
|
fun relaunchCleanApp(activity: HomeActivityIntentTestRule) {
|
||||||
|
closeApp(activity)
|
||||||
|
Intents.release()
|
||||||
|
activity.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 clickSnackbarButton(expectedText: String) =
|
||||||
|
clickPageObject(itemWithResIdAndText("$packageName:id/snackbar_btn", expectedText))
|
||||||
|
|
||||||
|
fun waitUntilSnackbarGone() {
|
||||||
|
mDevice.findObject(
|
||||||
|
UiSelector().resourceId("$packageName:id/snackbar_layout"),
|
||||||
|
).waitUntilGone(waitingTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verifySnackBarText(expectedText: String) {
|
||||||
|
assertTrue(
|
||||||
|
mDevice.findObject(
|
||||||
|
UiSelector()
|
||||||
|
.textContains(expectedText),
|
||||||
|
).waitForExists(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
|
fun deleteDownloadedFileOnStorage(fileName: String) {
|
||||||
|
val storageManager: StorageManager? = appContext.getSystemService(Context.STORAGE_SERVICE) as StorageManager?
|
||||||
|
val storageVolumes = storageManager!!.storageVolumes
|
||||||
|
val storageVolume: StorageVolume = storageVolumes[0]
|
||||||
|
val file = File(storageVolume.directory!!.path + "/Download/" + fileName)
|
||||||
|
try {
|
||||||
|
if (file.exists()) {
|
||||||
|
file.delete()
|
||||||
|
Log.d("TestLog", "File delete try 1")
|
||||||
|
assertFalse("The file was not deleted", file.exists())
|
||||||
|
}
|
||||||
|
} catch (e: AssertionError) {
|
||||||
|
file.delete()
|
||||||
|
Log.d("TestLog", "File delete retried")
|
||||||
|
assertFalse("The file was not deleted", file.exists())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertYoutubeAppOpens() = intended(toPackage(YOUTUBE_APP))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run test with automatically registering idling resources and cleanup.
|
||||||
|
*
|
||||||
|
* @param idlingResources zero or more [IdlingResource] to be used when running [testBlock].
|
||||||
|
* @param testBlock test code to execute.
|
||||||
|
*/
|
||||||
|
fun registerAndCleanupIdlingResources(
|
||||||
|
vararg idlingResources: IdlingResource,
|
||||||
|
testBlock: () -> Unit,
|
||||||
|
) {
|
||||||
|
idlingResources.forEach {
|
||||||
|
IdlingRegistry.getInstance().register(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
testBlock()
|
||||||
|
} finally {
|
||||||
|
idlingResources.forEach {
|
||||||
|
IdlingRegistry.getInstance().unregister(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, argument: String = appName) = appContext.resources.getString(id, argument)
|
||||||
|
|
||||||
|
// Permission allow dialogs differ on various Android APIs
|
||||||
|
fun grantSystemPermission() {
|
||||||
|
val whileUsingTheAppPermissionButton: UiObject =
|
||||||
|
mDevice.findObject(UiSelector().textContains("While using the app"))
|
||||||
|
|
||||||
|
val allowPermissionButton: UiObject =
|
||||||
|
mDevice.findObject(
|
||||||
|
UiSelector()
|
||||||
|
.textContains("Allow")
|
||||||
|
.className("android.widget.Button"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= 23) {
|
||||||
|
if (whileUsingTheAppPermissionButton.waitForExists(waitingTimeShort)) {
|
||||||
|
whileUsingTheAppPermissionButton.click()
|
||||||
|
} else if (allowPermissionButton.waitForExists(waitingTimeShort)) {
|
||||||
|
allowPermissionButton.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permission deny dialogs differ on various Android APIs
|
||||||
|
fun denyPermission() {
|
||||||
|
mDevice.findObject(UiSelector().textContains("Deny")).waitForExists(waitingTime)
|
||||||
|
mDevice.findObject(UiSelector().textContains("Deny")).click()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isTestLab(): Boolean {
|
||||||
|
return Settings.System.getString(appContext.contentResolver, "firebase.test.lab").toBoolean()
|
||||||
|
}
|
||||||
|
|
||||||
|
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("")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the default language of the entire device, not just the app.
|
||||||
|
* Runs on Debug variant as we don't want to adjust Release permission manifests
|
||||||
|
* Runs the test in its testBlock.
|
||||||
|
* Cleans up and sets the default locale after it's done.
|
||||||
|
*/
|
||||||
|
fun runWithSystemLocaleChanged(locale: Locale, testRule: ActivityTestRule<HomeActivity>, testBlock: () -> Unit) {
|
||||||
|
if (Config.channel.isDebug) {
|
||||||
|
/* Sets permission to change device language */
|
||||||
|
PermissionRequester().apply {
|
||||||
|
addPermissions(
|
||||||
|
Manifest.permission.CHANGE_CONFIGURATION,
|
||||||
|
)
|
||||||
|
requestPermissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
val defaultLocale = Locale.getDefault()
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSystemLocale(locale)
|
||||||
|
testBlock()
|
||||||
|
ThreadUtils.runOnUiThread { testRule.activity.recreate() }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
} finally {
|
||||||
|
setSystemLocale(defaultLocale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the default language of the entire device, not just the app.
|
||||||
|
*/
|
||||||
|
fun setSystemLocale(locale: Locale) {
|
||||||
|
val activityManagerNative = Class.forName("android.app.ActivityManagerNative")
|
||||||
|
val am = activityManagerNative.getMethod("getDefault", *arrayOfNulls(0))
|
||||||
|
.invoke(activityManagerNative, *arrayOfNulls(0))
|
||||||
|
val config = InstrumentationRegistry.getInstrumentation().context.resources.configuration
|
||||||
|
config.javaClass.getDeclaredField("locale")[config] = locale
|
||||||
|
config.javaClass.getDeclaredField("userSetLocale").setBoolean(config, true)
|
||||||
|
am.javaClass.getMethod(
|
||||||
|
"updateConfiguration",
|
||||||
|
Configuration::class.java,
|
||||||
|
).invoke(am, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates clipboard data.
|
||||||
|
*/
|
||||||
|
fun setTextToClipBoard(context: Context, message: String) {
|
||||||
|
val clipBoard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clipData = ClipData.newPlainText("label", message)
|
||||||
|
|
||||||
|
clipBoard.setPrimaryClip(clipData)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns sponsored shortcut title based on the index.
|
||||||
|
*/
|
||||||
|
fun getSponsoredShortcutTitle(position: Int): String {
|
||||||
|
val sponsoredShortcut = mDevice.findObject(
|
||||||
|
UiSelector()
|
||||||
|
.resourceId("$packageName:id/top_site_item")
|
||||||
|
.index(position - 1),
|
||||||
|
).getChild(
|
||||||
|
UiSelector()
|
||||||
|
.resourceId("$packageName:id/top_site_title"),
|
||||||
|
).text
|
||||||
|
|
||||||
|
return sponsoredShortcut
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verifyLightThemeApplied(expected: Boolean) =
|
||||||
|
assertFalse("Light theme not selected", expected)
|
||||||
|
|
||||||
|
fun verifyDarkThemeApplied(expected: Boolean) = assertTrue("Dark theme not selected", expected)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for tests to run only when certain conditions are met.
|
||||||
|
* For example: this method will avoid accidentally running a test on GV versions where the feature is disabled.
|
||||||
|
*/
|
||||||
|
fun runWithCondition(condition: Boolean, testBlock: () -> Unit) {
|
||||||
|
if (condition) {
|
||||||
|
testBlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putAppToBackground() {
|
||||||
|
mDevice.pressRecentApps()
|
||||||
|
mDevice.findObject(UiSelector().resourceId("$packageName:id/container")).waitUntilGone(waitingTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bringAppToForeground() {
|
||||||
|
mDevice.pressRecentApps()
|
||||||
|
mDevice.findObject(UiSelector().resourceId("$packageName:id/container")).waitForExists(waitingTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verifyKeyboardVisibility(isExpectedToBeVisible: Boolean = true) {
|
||||||
|
mDevice.waitForIdle()
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"Keyboard not shown",
|
||||||
|
isExpectedToBeVisible,
|
||||||
|
mDevice
|
||||||
|
.executeShellCommand("dumpsys input_method | grep mInputShown")
|
||||||
|
.contains("mInputShown=true"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of Search engines for the "home" region of the user.
|
||||||
|
* For en-us it will return the 6 engines selected by default: Google, Bing, DuckDuckGo, Amazon, Ebay, Wikipedia.
|
||||||
|
*/
|
||||||
|
fun getRegionSearchEnginesList(): List<SearchEngine> {
|
||||||
|
val searchEnginesList = appContext.components.core.store.state.search.regionSearchEngines
|
||||||
|
assertTrue("Search engines list returned nothing", searchEnginesList.isNotEmpty())
|
||||||
|
return searchEnginesList
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of Search engines available to be added by user choice.
|
||||||
|
* For en-us it will return the 2 engines: Reddit, Youtube.
|
||||||
|
*/
|
||||||
|
fun getAvailableSearchEngines(): List<SearchEngine> {
|
||||||
|
val searchEnginesList = appContext.components.core.store.state.search.availableSearchEngines
|
||||||
|
assertTrue("Search engines list returned nothing", searchEnginesList.isNotEmpty())
|
||||||
|
return searchEnginesList
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 org.mozilla.fenix.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 org.mozilla.fenix.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 org.mozilla.fenix.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 org.mozilla.fenix.helpers.ext
|
||||||
|
|
||||||
|
import androidx.test.uiautomator.SearchCondition
|
||||||
|
import androidx.test.uiautomator.UiDevice
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.mozilla.fenix.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,47 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.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,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 org.mozilla.fenix.helpers.idlingresource
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.view.View.VISIBLE
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.test.espresso.IdlingResource
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.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 org.mozilla.fenix.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,50 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.helpers.idlingresource
|
||||||
|
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import androidx.test.espresso.IdlingResource
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import org.mozilla.fenix.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 org.mozilla.fenix.helpers.matchers
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.graphics.drawable.StateListDrawable
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.test.espresso.matcher.BoundedMatcher
|
||||||
|
import org.hamcrest.Description
|
||||||
|
|
||||||
|
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 org.mozilla.fenix.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 org.mozilla.fenix.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,126 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.onboarding.view
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mozilla.experiments.nimbus.StringHolder
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
|
||||||
|
import org.mozilla.fenix.nimbus.OnboardingCardData
|
||||||
|
import org.mozilla.fenix.nimbus.OnboardingCardType
|
||||||
|
|
||||||
|
class JunoOnboardingMapperTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val activityTestRule =
|
||||||
|
HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showNotificationTrue_showAddWidgetFalse_pagesToDisplay_returnsSortedListOfAllConvertedPages_withoutAddWidgetPage() {
|
||||||
|
val expected = listOf(defaultBrowserPageUiData, syncPageUiData, notificationPageUiData)
|
||||||
|
assertEquals(expected, unsortedAllKnownCardData.toPageUiData(true, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showNotificationFalse_showAddWidgetFalse_pagesToDisplay_returnsSortedListOfConvertedPages_withoutNotificationPage_and_addWidgetPage() {
|
||||||
|
val expected = listOf(defaultBrowserPageUiData, syncPageUiData)
|
||||||
|
assertEquals(expected, unsortedAllKnownCardData.toPageUiData(false, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showNotificationFalse_showAddWidgetTrue_pagesToDisplay_returnsSortedListOfAllConvertedPages_withoutNotificationPage() {
|
||||||
|
val expected = listOf(defaultBrowserPageUiData, addSearchWidgetPageUiData, syncPageUiData)
|
||||||
|
assertEquals(expected, unsortedAllKnownCardData.toPageUiData(false, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showNotificationTrue_and_showAddWidgetTrue_pagesToDisplay_returnsSortedListOfConvertedPages() {
|
||||||
|
val expected = listOf(defaultBrowserPageUiData, addSearchWidgetPageUiData, syncPageUiData, notificationPageUiData)
|
||||||
|
assertEquals(expected, unsortedAllKnownCardData.toPageUiData(true, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val defaultBrowserPageUiData = OnboardingPageUiData(
|
||||||
|
type = OnboardingPageUiData.Type.DEFAULT_BROWSER,
|
||||||
|
imageRes = R.drawable.ic_onboarding_welcome,
|
||||||
|
title = "default browser title",
|
||||||
|
description = "default browser body with link text",
|
||||||
|
linkText = "link text",
|
||||||
|
primaryButtonLabel = "default browser primary button text",
|
||||||
|
secondaryButtonLabel = "default browser secondary button text",
|
||||||
|
)
|
||||||
|
private val addSearchWidgetPageUiData = OnboardingPageUiData(
|
||||||
|
type = OnboardingPageUiData.Type.ADD_SEARCH_WIDGET,
|
||||||
|
imageRes = R.drawable.ic_onboarding_search_widget,
|
||||||
|
title = "add search widget title",
|
||||||
|
description = "add search widget body with link text",
|
||||||
|
linkText = "link text",
|
||||||
|
primaryButtonLabel = "add search widget primary button text",
|
||||||
|
secondaryButtonLabel = "add search widget secondary button text",
|
||||||
|
)
|
||||||
|
private val syncPageUiData = OnboardingPageUiData(
|
||||||
|
type = OnboardingPageUiData.Type.SYNC_SIGN_IN,
|
||||||
|
imageRes = R.drawable.ic_onboarding_sync,
|
||||||
|
title = "sync title",
|
||||||
|
description = "sync body",
|
||||||
|
primaryButtonLabel = "sync primary button text",
|
||||||
|
secondaryButtonLabel = "sync secondary button text",
|
||||||
|
)
|
||||||
|
private val notificationPageUiData = OnboardingPageUiData(
|
||||||
|
type = OnboardingPageUiData.Type.NOTIFICATION_PERMISSION,
|
||||||
|
imageRes = R.drawable.ic_notification_permission,
|
||||||
|
title = "notification title",
|
||||||
|
description = "notification body",
|
||||||
|
primaryButtonLabel = "notification primary button text",
|
||||||
|
secondaryButtonLabel = "notification secondary button text",
|
||||||
|
)
|
||||||
|
|
||||||
|
private val defaultBrowserCardData = OnboardingCardData(
|
||||||
|
cardType = OnboardingCardType.DEFAULT_BROWSER,
|
||||||
|
imageRes = R.drawable.ic_onboarding_welcome,
|
||||||
|
title = StringHolder(null, "default browser title"),
|
||||||
|
body = StringHolder(null, "default browser body with link text"),
|
||||||
|
linkText = StringHolder(null, "link text"),
|
||||||
|
primaryButtonLabel = StringHolder(null, "default browser primary button text"),
|
||||||
|
secondaryButtonLabel = StringHolder(null, "default browser secondary button text"),
|
||||||
|
ordering = 10,
|
||||||
|
)
|
||||||
|
private val addSearchWidgetCardData = OnboardingCardData(
|
||||||
|
cardType = OnboardingCardType.ADD_SEARCH_WIDGET,
|
||||||
|
imageRes = R.drawable.ic_onboarding_search_widget,
|
||||||
|
title = StringHolder(null, "add search widget title"),
|
||||||
|
body = StringHolder(null, "add search widget body with link text"),
|
||||||
|
linkText = StringHolder(null, "link text"),
|
||||||
|
primaryButtonLabel = StringHolder(null, "add search widget primary button text"),
|
||||||
|
secondaryButtonLabel = StringHolder(null, "add search widget secondary button text"),
|
||||||
|
ordering = 15,
|
||||||
|
)
|
||||||
|
private val syncCardData = OnboardingCardData(
|
||||||
|
cardType = OnboardingCardType.SYNC_SIGN_IN,
|
||||||
|
imageRes = R.drawable.ic_onboarding_sync,
|
||||||
|
title = StringHolder(null, "sync title"),
|
||||||
|
body = StringHolder(null, "sync body"),
|
||||||
|
primaryButtonLabel = StringHolder(null, "sync primary button text"),
|
||||||
|
secondaryButtonLabel = StringHolder(null, "sync secondary button text"),
|
||||||
|
ordering = 20,
|
||||||
|
)
|
||||||
|
private val notificationCardData = OnboardingCardData(
|
||||||
|
cardType = OnboardingCardType.NOTIFICATION_PERMISSION,
|
||||||
|
imageRes = R.drawable.ic_notification_permission,
|
||||||
|
title = StringHolder(null, "notification title"),
|
||||||
|
body = StringHolder(null, "notification body"),
|
||||||
|
primaryButtonLabel = StringHolder(null, "notification primary button text"),
|
||||||
|
secondaryButtonLabel = StringHolder(null, "notification secondary button text"),
|
||||||
|
ordering = 30,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val unsortedAllKnownCardData = listOf(
|
||||||
|
syncCardData,
|
||||||
|
notificationCardData,
|
||||||
|
defaultBrowserCardData,
|
||||||
|
addSearchWidgetCardData,
|
||||||
|
)
|
|
@ -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 org.mozilla.fenix.perf
|
||||||
|
|
||||||
|
import androidx.benchmark.junit4.BenchmarkRule
|
||||||
|
import androidx.benchmark.junit4.measureRepeated
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To run this benchmark:
|
||||||
|
* - Comment out @Ignore: DO NOT COMMIT THIS!
|
||||||
|
* - See run instructions in app/benchmark.gradle
|
||||||
|
*
|
||||||
|
* See https://developer.android.com/studio/profile/benchmark#write-benchmark for how to write a
|
||||||
|
* real benchmark, including testing UI code. See
|
||||||
|
* https://developer.android.com/studio/profile/benchmark#what-to-benchmark for when jetpack
|
||||||
|
* microbenchmark is a good fit.
|
||||||
|
*/
|
||||||
|
@Ignore("This is a sample: we don't want it to run when we run all the tests")
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class SampleBenchmark {
|
||||||
|
@get:Rule
|
||||||
|
val benchmarkRule = BenchmarkRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun additionBenchmark() = benchmarkRule.measureRepeated {
|
||||||
|
var i = 0
|
||||||
|
while (i < 10_000_000) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 org.mozilla.fenix.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 org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.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 = 16
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* Firefox 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..2 // 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,189 @@
|
||||||
|
/* 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 org.mozilla.fenix.screenshots
|
||||||
|
|
||||||
|
import android.os.SystemClock
|
||||||
|
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.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
|
||||||
|
import org.mozilla.fenix.helpers.HomeActivityTestRule
|
||||||
|
import org.mozilla.fenix.helpers.TestAssetHelper
|
||||||
|
import org.mozilla.fenix.ui.robots.bookmarksMenu
|
||||||
|
import org.mozilla.fenix.ui.robots.homeScreen
|
||||||
|
import org.mozilla.fenix.ui.robots.navigationToolbar
|
||||||
|
import org.mozilla.fenix.ui.robots.swipeToBottom
|
||||||
|
import tools.fastlane.screengrab.Screengrab
|
||||||
|
import tools.fastlane.screengrab.locale.LocaleTestRule
|
||||||
|
|
||||||
|
class ComposeMenuScreenShotTest : ScreenshotTest() {
|
||||||
|
private lateinit var mockWebServer: MockWebServer
|
||||||
|
private lateinit var mDevice: UiDevice
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val localeTestRule = LocaleTestRule()
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val composeTestRule =
|
||||||
|
AndroidComposeTestRule(
|
||||||
|
HomeActivityTestRule.withDefaultSettingsOverrides(
|
||||||
|
tabsTrayRewriteEnabled = true,
|
||||||
|
),
|
||||||
|
) { it.activity }
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||||
|
mockWebServer = MockWebServer().apply {
|
||||||
|
dispatcher = AndroidAssetDispatcher()
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
composeTestRule.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")
|
||||||
|
}.openSettingsSubMenuDataCollection {
|
||||||
|
Screengrab.screenshot("settings-telemetry")
|
||||||
|
}.goBack {
|
||||||
|
}.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")
|
||||||
|
saveNewFolder()
|
||||||
|
}.openThreeDotMenu("test") {
|
||||||
|
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") {
|
||||||
|
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")
|
||||||
|
}.openComposeTabDrawer(composeTestRule) {
|
||||||
|
TestAssetHelper.waitingTime
|
||||||
|
Screengrab.screenshot("TabDrawerRobot_one-tab-open")
|
||||||
|
}.openThreeDotMenu {
|
||||||
|
TestAssetHelper.waitingTime
|
||||||
|
Screengrab.screenshot("TabDrawerRobot_three-dot-menu")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun tabMenuTest() {
|
||||||
|
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
|
||||||
|
navigationToolbar {
|
||||||
|
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
|
||||||
|
}.openThreeDotMenu {
|
||||||
|
Screengrab.screenshot("TabDrawerRobot_browser-tab-menu")
|
||||||
|
}.closeBrowserMenuToBrowser {
|
||||||
|
}.openComposeTabDrawer(composeTestRule) {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue