first commit

master
harvey186 2023-11-28 09:06:47 +01:00
commit 52b902effa
2513 changed files with 488007 additions and 0 deletions

154
.buildconfig.yml Normal file
View File

@ -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

6
.editorconfig Normal file
View File

@ -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

1
.gitattributes vendored Normal file
View File

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

107
.gitignore vendored Normal file
View File

@ -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

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "android-components"]
path = android-components
url = https://github.com/akliuxingyuan/android-components

15
CODE_OF_CONDUCT.md Normal file
View File

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

3
Gemfile Normal file
View File

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

53
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,53 @@
pipeline {
agent any
triggers {
cron(env.BRANCH_NAME == 'main' ? 'H 0 * * *' : '')
}
options {
timestamps()
timeout(time: 1, unit: 'HOURS')
}
stages {
stage('test') {
when { branch 'main' }
steps {
dir('app/src/androidTest/java/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/)")
}
}
}

31
README.md Normal file
View File

@ -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/

5
SECURITY.md Normal file
View File

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

1
android-components Submodule

@ -0,0 +1 @@
Subproject commit 69f2dd41c44d9a8b690527493d1d423d857d5e97

239
app/.experimenter.yaml Normal file
View File

@ -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."

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

81
app/benchmark.gradle Normal file
View File

@ -0,0 +1,81 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
// This comment contains the central documentation for how we configured Jetpack Benchmark. Currently:
// - microbenchmark: configured differently than recommended (see inline notes below)
// - macrobenchmark: not configured
//
// To run our benchmarks, you need to set the "benchmark" gradle property. You can:
// - (preferred) Run via the command line (change the class you run on):
// ./gradlew -Pbenchmark app:connectedCheck -Pandroid.testInstrumentationRunnerArguments.class=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'
]
}
}

913
app/build.gradle Normal file
View File

@ -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&regexp=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&regexp=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

2610
app/github.txt Normal file

File diff suppressed because it is too large Load Diff

1363
app/lint-baseline.xml Normal file

File diff suppressed because it is too large Load Diff

70
app/lint.xml Normal file
View File

@ -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>

11025
app/metrics.yaml Normal file

File diff suppressed because it is too large Load Diff

483
app/nimbus.fml.yaml Normal file
View File

@ -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.

114
app/onboarding.fml.yaml Normal file
View File

@ -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.

19
app/pbm.fml.yaml Normal file
View File

@ -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

93
app/pings.yaml Normal file
View File

@ -0,0 +1,93 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
---
$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

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

@ -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.** { *; }

View File

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

View File

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

View File

@ -0,0 +1,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>

View File

@ -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>

View File

@ -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>

View File

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

View File

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

View File

@ -0,0 +1,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>

View File

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

View File

@ -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

View File

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

View File

@ -0,0 +1,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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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>

View File

@ -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

View File

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

View File

@ -0,0 +1,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"
}
}

View File

@ -0,0 +1,23 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package 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)
}
}

View File

@ -0,0 +1,13 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package 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

View File

@ -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)
}
}
}

View File

@ -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"

View 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"
}
}
}

View File

@ -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()
}
}

View File

@ -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 _

View File

@ -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))

View File

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

View File

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

View File

@ -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")

View File

@ -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")

View File

@ -0,0 +1,3 @@
urls:
stage_server: "https://stage.experimenter.nonprod.dataops.mozgcp.net"
prod_server: "https://experimenter.services.mozilla.com"

View File

@ -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!!)
}
}

View File

@ -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."))
}
}
}

View File

@ -0,0 +1,32 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package 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)
}
}
}

View File

@ -0,0 +1,48 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package 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",
)
}
}

View File

@ -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)
}
}

View File

@ -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,
}

View File

@ -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()
}

View File

@ -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()
}
}

View File

@ -0,0 +1,38 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@file:Suppress("DEPRECATION")
package 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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -0,0 +1,92 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package 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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -0,0 +1,111 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package 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)
}
}

View File

@ -0,0 +1,108 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package 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"
}
}

View File

@ -0,0 +1,32 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package 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
}
}

View File

@ -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()
}
}
}

View File

@ -0,0 +1,64 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package 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
}

View File

@ -0,0 +1,48 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package 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
}
}

View File

@ -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, "", "")
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,36 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package 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
}
}

View File

@ -0,0 +1,45 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package 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
}
}

View File

@ -0,0 +1,48 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package 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,
),
)

View File

@ -0,0 +1,20 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package 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))

View File

@ -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
}
}

View File

@ -0,0 +1,48 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package 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
}
}

View File

@ -0,0 +1,56 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package 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)
}
}

View File

@ -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()
}
}

View File

@ -0,0 +1,39 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package 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
}
}

View File

@ -0,0 +1,39 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package 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
}
}

View File

@ -0,0 +1,33 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package 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
}
}
}

View File

@ -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,
)

View File

@ -0,0 +1,38 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package 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++
}
}
}

View File

@ -0,0 +1,159 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package 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.
"""

View File

@ -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